Java学习笔记--多线程

线程的生命周期

线程的声明周期共有 6 种状态,分别是:新建New运行(可运行)Runnable阻塞Blocked计时等待Timed Waiting等待Waiting终止Terminate

  • 当你声明一个线程对象时,线程处于新建状态,系统不会为它分配资源,它只是一个空的线程对象。
  • 调用start()方法时,线程就成为了可运行状态,至于是否是运行状态,则要看系统的调度了。
  • 调用了sleep()方法、调用wait()方法和 IO 阻塞时,线程处于等待、计时等待或阻塞状态。
  • 当run()方法执行结束后,线程也就终止了。

一、多线程的实现

Java中的Thread类就是专门用来创建线程和操作线程的类。

  • 创建线程的方法:
  1. 继承Thread类并重写它的run()方法,然后用这个子类来创建对象并调用start()方法。
  2. 定义一个类并实现Runnable接口,实现run()方法。

总结:总的来说就是线程通过start()方法启动而不是run()方法,run()方法的内容为我们要实现的业务逻辑。

示例:

public class ThreadStudy {

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread thread = new Thread(new Thread2());

        thread1.start();
        thread.start();
    }

}

class Thread1 extends Thread {
    @Override
    public void run() {
        super.run();

        //这里我们把两个线程各自的工作设置为打印100次信息
        for (int i = 0; i < 100; ++i) {
            System.out.println("Hello! This is " + i);
        }
    }
}

class Thread2 implements Runnable {
    @Override
    public void run() {
        //这里我们把两个线程各自的工作设置为打印100次信息
        for (int i = 0; i < 100; ++i) {
            System.out.println("Thanks! This is " + i);
        }
    }
}

二、ThreadLocal

即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下载通过get()方法获取到原先设置的值。

示例:

private void threadLocalTest() {
    Thread3 thread3 = new Thread3();
    //启动两个线程
    new Thread(thread3).start();
    new Thread(thread3).start();
}

class Thread3 implements Runnable {

    //初始化一个ThreadLocal对象,初始值为0
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取线程变量的值
            Integer integer = threadLocal.get();
            integer += 1;
            //设置线程变量的值
            threadLocal.set(integer);
            System.out.println(integer);
        }
    }
}

三、线程同步

当多个线程操作同一个对象时,就会出现线程安全问题,被多个线程同时操作的对象数据可能会发生错误。线程同步可以保证在同一个时刻该对象只被一个线程访问。

  • Synchronized
    关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。它有三种使用方法:
  • 对普通方式使用,将会锁住当前实例对象
  • 对静态方法使用,将会锁住当前类的Class对象
  • 对代码块使用,将会锁住代码块中的对象

示例:

public class SynchronizedDemo {
    private static Object lock = new Object();

    public static void main(String[] args) {
        //同步代码块 锁住lock
        synchronized (lock) {
            //doSomething
        }
    }

    //静态同步方法  锁住当前类class对象
    public synchronized static void staticMethod(){

    }
    //普通同步方法  锁住当前实例对象
    public synchronized void memberMethod() {

    }
}

四、Lock 与 Unlock

JUC 中的 ReentrantLock 是多线程编程中常用的加锁方式,ReentrantLock 加锁比 synchronized 加锁更加的灵活,提供了更加丰富的功能。

示例:

 private static ReentrantLock lock = new ReentrantLock();

private void lockTest() {
    Thread thread = new Thread(() -> {
        lock.lock();
        try {
            System.out.println("线程1加锁");
        }finally {
            lock.unlock();
            System.out.println("线程1解锁");
        }
    });
    Thread thread1 = new Thread(()->{
        lock.lock();
        try {
            System.out.println("线程2加锁");
        } finally {
            lock.unlock();
            System.out.println("线程2解锁");
        }
    });
    thread1.start();
    thread.start();
}

五、死锁

当死锁发生时,系统将会瘫痪。比如两个线程互相等待对方释放锁。

示例:

private static Object lockA = new Object();
    private static Object lockB = new Object();

    public static void main(String[] args) {
        //这里使用lambda表达式创建了一个线程  
        //线程  1
        new Thread(() -> {
            synchronized (lockA) {
                try {
                    //线程休眠一段时间  确保另外一个线程可以获取到b锁
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("D");
                synchronized (lockB) {
                }
            }
        }).start();
        //线程 2
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("死锁...");
                synchronized (lockA) {
                }
            }
        }).start();
    }

六、饥饿

饥饿是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。

比如当前线程处于一个低优先级的情况下,操作系统每次都调用高优先级的线程运行,就会导致当前线程虽然可以运行,但是一直不能被运行的情况。

七、ArrayBlockingQueue

是由数组支持的有界阻塞队列。

  • 构造方法:
  • public ArrayBlockingQueue(int capacity):构造大小为capacity的队列
  • public ArrayBlockingQueue(int capacity, boolean fair):指定队列大小,以及内部实现是公平锁还是非公平锁
  • public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c):指定队列大小,以及锁实现,并且在初始化是加入集合c
  • 入队常用方法:
入队方法 队列已满 队列未满
add 抛出异常 返回true
offer 返回false 返回true
put 阻塞直到插入 没有返回值
  • 出队当用方法:
出队方法 队列为空 队列不为空
remove 抛出异常 移出并返回队首
poll 返回null 移出并返回队首
take 阻塞直到返回 移出并返回队首

使用示例:

//构建大小为10的阻塞队列
    private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                queue.add(i);
            }
        });
        thread1.start();
        try {
            //等待线程1执行完毕
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            if (!queue.offer(11)) {
                System.out.println("插入元素11失败");
            }
            try {
                queue.put(11);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        Thread thread2 = new Thread(() -> {
           Integer element;
            System.out.println("开始出队:");
            while ((element = queue.poll()) != null) {
                System.out.print("\t" + element);
            }
        });
        thread2.start();
    }

八、LinkedBlockingQueue

LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:

  • 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
  • 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
  • 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
  • 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

相关链接:http://benjaminwhx.com/2018/05/11/【细谈Java并发】谈谈LinkedBlockingQueue/

九、生成者消费者模式

生产者消费者模式是多线程编程中非常重要的设计模式,生产者负责生产数据,消费者负责消费数据。生产者消费者模式中间通常还有一个缓冲区,用于存放生产者生产的数据,而消费者则从缓冲区中获取,这样可以降低生产者和消费者之间的耦合度。 举个例子来说吧,比如有厂家,代理商,顾客,厂家就是生产者,顾客就是消费者,代理商就是缓冲区,顾客从代理商这里买东西,代理商负责从厂家处拿货,并且销售给顾客,顾客不用直接和厂家打交道,并且通过代理商,就可以直接获取商品,或者从代理商处知道货物不足,需要等待。

示例:

//阻塞队列
    private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

    public static void main(String[] args) {
        //生产者
        Thread provider = new Thread(() -> {
            Random random = new Random();
            for (int j = 0; j < 5; j++) {
                try {
                    int i = random.nextInt();
                    //注释直到插入数据
                    queue.put(i);
                    System.out.println("生产数据:" + i);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //消费者
        Thread consumer = new Thread(() -> {
            Integer data;
            for (int i = 0; i < 5; i++) {
                try {
                    //阻塞直到取出数据
                    data = queue.take();
                    System.out.println("消费数据:" + data);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动线程
        provider.start();
        consumer.start();
    }

十、线程池

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

  • 使用Executors工具类创建
  • newFixedThreadPool(int nThreads):创建一个固定大小为 n 的线程池
  • newSingleThreadExecutor():创建只有一个线程的线程池
  • newCachedThreadPool():创建一个根据需要创建新线程的线程池

示例:

//使用Executors 创建一个固定大小为5的线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(5);

public static void main(String[] args) {
    // 提交任务
    executorService.submit(() -> {
        for (int i = 0; i < 10; i++) {
            System.out.print(i + " ");
        }
    });
    //停止线程池 并不会立即关闭 ,而是在线程池中的任务执行完毕后才关闭
    executorService.shutdown();
}
  • 直接创建线程池

示例:

private static ExecutorService executorService = new ThreadPoolExecutor(5,   //核心线程数为5
            10,     //最大线程数为10
            0L, TimeUnit.MILLISECONDS, //非核心线程存活时间
            new LinkedBlockingQueue<>());  //任务队列

public static void main(String[] args) {
    executorService.submit(() -> {
        for (int i = 0; i < 10; i++) {
            System.out.print(i + "  ");
        }
    });
    executorService.shutdown();
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容