Java 多线程那点事

一、首先讲解 Volatile 关键字,上Demo,大家思考下,"子线程结束"这句话能不能打印呢?

public class VolatileTest {
    public static void main(String[] args) {
		MyVolatileRunnable myVolatileRunnable = new MyVolatileRunnable();
        new Thread(myVolatileRunnable).start();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        myVolatileRunnable.isRunning = false;
        System.out.println("主线程执行完成");
	}
	static class MyVolatileRunnable implements Runnable{
        public boolean isRunning = true;
        @Override
        public void run() {
            while (isRunning);
            // 99% 都不会打印
            System.out.println("子线程结束");
        }
    }
 }

分析:
1、每个线程都有自己的工作内存,当一个线程执行的时候,首先它会把主内存的值copy一份到自己的工作内存中去。
2、虽然主线程修改这个值,但并没有把值写入到主内存中去
3、子线程在执行的时候,依旧使用工作内存中的值,也就是 true,所以while循环永远不会跳出。
4、解决办法: public volatile boolean isRunning = true; 加上volatile关键字。
结论:volatile 此关键字的作用是对其他线程可见,也就是说主线程修改了这个值之后,就会把这个值写入到主内存中去,同时告知其他使用到这个值的线程,此值已经失效,需要从主内存中从新读取这个值。

二、volatile 和 synchronize 关键字的区别,上Demo,大家思考下count值一定会加到100 吗?

public class VolatileTest {
    public static void main(String[] args) {
   		 for (int i = 0; i < 100; i++) {
            new Thread(new MyRunnable()).start();
        }
    }
    static class MyRunnable implements Runnable{
        private static volatile int count = 0;
        @Override
        public void run() {
            /*
             * count++ 这个操作不属于原子操作,所以就有可能出现最终的值加不到100
             * */
            count++;
            System.out.println("lgj count :" + count);
            /*
            * count++ CPU要执行3条指令,相当于如下代码
            * count = count + 1;
            * count + 1 是一个指令,当第一个线程执行完成之后,还没有把这个值赋值给count的时候
            * 第二个线程抢占到CPU的时间片,然后执行count++,读取到的值不是自增后的值
            * 所以导致,count的最终值不一定是 100
            * */
        }
    }
}

分析:上面有分析结论,主要因为 count++ 操作不属于原子操作,所以有可能导致最终计算的值没有达到100,所以 volatile 关键字不能保证操作的原子性,解决办法需要加锁 synchronize ,注意需要保证锁对象唯一
为什么加锁就可以解决这个问题呢?
1、当第一个线程执行 代码块的时候,他会把这个值从主内存中,读取一份到工作内存中
2、当第二个线程执行这块代码的时候,因为没有拿到锁,就处于阻塞状态
3、当第一个线程执行完成之后,又把这个值从新写会主内存中,释放锁
4、当第二个线程拿到锁之后,又会把这个值,从主内存中读取一份到工作内存中
5、这样就能保住这个值操作的原子性,以及对其他线程的可见性

两个关键字的区别以及使用场景:
volatile 不能保证操作的原子性,但可以保证对其他线程的可见性,适用于一写多读的场景。
synchronized 可以保证 代码块的原子性 以及对其他线程的可见性,适用于同步一整块操作。

三、原子操作工具类,AtomicInteger上Demo

public class UserAtomicInt {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        /*
        * 原子类的特点,两个线程同时自增 AtomicInteger 的值
        * 最后的值依然是 20000,不存在多线下,自增值不一样的情况
        * */
        Thread thread = new Thread(myRunnable);
        Thread thread2 = new Thread(myRunnable);
        thread.start();
        thread2.start();
        try {
            thread.join();// 等待thread线程执行完成之后,主线程才会执行
            thread2.join();
        }catch (Exception e){
        }
        int i = myRunnable.ai.get();
        System.out.println("i : " + i);

    }

    static class MyRunnable implements Runnable{
        public AtomicInteger ai = new AtomicInteger(0);
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                // 先自增,然后获取
                ai.incrementAndGet();
            }
        }
    }
}

结论:当多线程操作的只是一个 int 值得时候,完全可以用原子工具类来替代,实战项目: CallTipsManager 类中用到过

**4、线程 join的用法,**在哪个线程执行join方法,那么该线程就处于等待状态,直到被加入的线程执行完毕后,才继续执行此线程。上Demo

public class ThreadJoinTest {
    static class AnMiao implements Runnable{
        @Override
        public void run() {
            System.out.println("安苗开始排队打饭.....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("安苗排队打饭完毕.....");
        }
    }
    public static void main(String[] args) {
        AnMiao anMiao = new AnMiao();
        Thread anMiaoThread = new Thread(anMiao);
        anMiaoThread.start();
        System.out.println("李国菁开始排队打饭.....");
        try {
            // 此时安苗插队进来,导致我这边打饭阻塞,一直等到安苗打饭完成之后,我才能打饭完毕
            anMiaoThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("李国菁排队打饭完毕.....");
    }
}

5、CountDownLatch 工具的用法,一般适用于等待初始化工作完成之后,然后操作一些其他的业务操作,上Demo

/**
 * 类说明:演示CountDownLatch用法,
 * 共5个初始化子线程,6个闭锁扣除点,扣除完毕后,主线程和业务线程才能继续执行
 */
public class UseCountDownLatch {
    static CountDownLatch latch = new CountDownLatch(6);
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread_" + Thread.currentThread().getId()
                        + " ready init 11111111111");
                latch.countDown();
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread_" + Thread.currentThread().getId()
                        + "  ready init 22222222222");
                latch.countDown();
            }
        }).start();
        new Thread(new BusiThread()).start();
        for (int i = 0; i < 4; i++) {
            Thread thread = new Thread(new InitThread());
            thread.start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main 做业务工作.........");
    }

    static class InitThread implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread_" + Thread.currentThread().getId()
                    + "做初始化工作");
            latch.countDown();
            System.out.println("Thread_" + Thread.currentThread().getId()
                    + " ........做其他工作..");
        }
    }

    /*业务线程等待latch的计数器为0完成*/
    static class BusiThread implements Runnable {
        @Override
        public void run() {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("latch : " + latch.getCount());
            System.out.println("BusiThread_" + Thread.currentThread().getId()
                    + " 做业务相关工作,需要初始化工作完成...");
        }
    }
}

6、线程池,线程池的优点
1、降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程池的创建各个参数含义
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用
TimeUnit
keepAliveTime的时间单位
workQueue
workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。
一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)更重要的,使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

int availableSize = Runtime.getRuntime().availableProcessors();
通常核心线程数量等于 availableSize + 1
最大线程数量等于 availableSize * 2 + 1

提交任务
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。

关闭线程池
shutdown 中断所有没有正在执行任务的线程,然后等待正在执行任务的线程执行完成之后,线程池关闭。

7、生产者和消费者,一个线程专门生产商品,一个线程专门消费商品,当生产的商品超过8个的时候,生产就会阻塞,当商品为0 的时候,消费者消费商品就会阻塞。

public class 生产者 extends Thread{
    // 一个由链表结构组成的有界阻塞队列
    private LinkedBlockingQueue<Integer> blockingQueue;
    public 生产者(LinkedBlockingQueue blockingQueue){
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                // Thread.sleep(500);
                // 这个方法是阻塞方法
                blockingQueue.put(i);
                System.out.println("放置完成 : " + blockingQueue.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class 消费者 extends Thread{
    // 一个由链表结构组成的有界阻塞队列
    private LinkedBlockingQueue<Integer> blockingQueue;
    public 消费者(LinkedBlockingQueue blockingQueue){
        this.blockingQueue = blockingQueue;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(50);
                // 这个方法是阻塞方法,当队列中没有的时候,就会阻塞
                Integer take = blockingQueue.take();
                System.out.println("消费 :" + take + "======" + blockingQueue.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TestConsumer {
    public static LinkedBlockingQueue<Integer> blockingQueue;
    public static void main(String[] args) {
        // 初始化一个 有界队列
        blockingQueue = new LinkedBlockingQueue<>(8);
        // 启动两个线程,生成者和消费者,当生成者往队列里面
        // 放置第9个商品的时候,就会阻塞,直到消费者完成一个商品的消费后,才能把这个商品放置进去
        生产者 生产者 = new 生产者(blockingQueue);
        消费者 消费者 = new 消费者(blockingQueue);
        生产者.start();
        消费者.start();
    }
}

以上就是先总结的线程那点事,后续继续补充,比如显示锁,以及CAS AQS 这些…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值