2、多线程原理与实践

第二章:线程安全、内置锁、线程通信

  1. 线程安全问题
    1. 什么是线程安全性问题
    2. 如何解决线程安全性问题
    3. 线程安全问题实例:SpringBean单例
  2. Synchronized内置锁
    1. synchronized同步方法
    2. synchronized同步代码块
    3. 两者区别和联系
    4. 静态synchronized同步方法
    5. 生产者和消费者问题
  3. 线程间通信
    1. 通知等待机制
    2. wait,sleep和join
    3. join的底层

1.线程安全问题

1.1什么是线程安全性问题

多线程同时对一个全局变量做写的操作,可能会受到其他线程的干扰,就会发生线程安全性问题

  • 全局变量:JMM中共享内存的资源
  • 写操作:修改操作
public class ThreadCount implements Runnable{
    public Integer count=0;

    @Override
    public void run() {
        for (int i = 0; i <= 5000; i++) {
            count++;
            System.out.println(Thread.currentThread().getName()+"count:"+count);
        }

    }
    public static void main(String[] args) {
        ThreadCount threadCount=new ThreadCount();
        Thread thread=new Thread(threadCount);
        Thread thread1=new Thread(threadCount);
        thread.start();
        thread1.start();
    }
}

请添加图片描述

本应该得到10002的结果,但是到了9876就结束了,原因分析:

会出现多个线程间同时修改共享变量,涉及到一个自增运算符线程不安全的问题

自增运算符是一个复合操作,至少包含三个JVM指令(涉及到JMM的知识)

  1. “内存取值”
  2. “寄存器加一”
  3. “存值到内存”

三条指令在JVM内部都是独立进行的,具有原子性,中间完全可能存在多个线程并发执行。

1.2如何解决线程安全性问题

核心思想:上锁(在分布式环境下 用分布式锁)

在同一个JVM中,多个线程需要竞争锁的资源,最终只能够有一个线程能够获取到锁,多个线程同时抢同一把,谁(线程)能够获取到锁,谁就能执行到改代码,如果没有获取锁成功,中间需要经历锁的升级过程。如果线程一直没有获取到锁,就会一直阻塞等待。

如果线程A获取到锁,但一直不释放锁。线程B一直获取不到锁,这会一直阻塞等待

关于临界资源,临界区

临界区资源,即受保护的对象,指代全局共享变量

临界区代码:是访问修改临界资源的代码(为了保证线程安全,要对这个临界区代码进行上锁)

解决线程安全性问题的方法(面试)

1.使用synchronized锁,在JDK1.6开始 内置锁有了锁升级的过程,大大提升了内置锁的性能

2.使用Lock锁(JUC包的),需要自己实现锁升级过程,底层通过AQS和CAS实现

3.通过ThreadLocal线程独占资源,需要注意内存泄漏问题

4.原子类(Atmic开头)CAS非阻塞式

1.3线程安全问题实例:Spring的单例Bean

像SpringMVC的Controller默认为单例,需要注意线程安全问题

为什么SpringBean单例会出现安全性问题(这里只考虑单体应用)

@RestController
@Slf4j
//@Scope(value = "prototype") 注明当前为单例,其实单例也是默认的
public class CountService {

    private int count = 0;
//出现全局共享变量,可能当前JVM的多个线程对该变量进行并发访问、修改
    @RequestMapping("/count")
    //解决:加锁。
    public synchronized String count() {
        try {
            log.info(">count<" + count++);
            try {
                Thread.sleep(3000);
            } catch (Exception e) {

            }
        } catch (Exception e) {

        }
        return "count";
    }
}

1.4字节码层面分析线程安全性问题

线程安全问题从底层无非就是:

  1. 多条字节码指令非原子性操作
  2. 线程的上下文切换导致 共享资源 被非原子性操作 随意修改
  3. JMM内存模型,共享内存和线程私有内存 之间共享变量的fetch、push

2.内置锁

java对象都隐含有一把锁,即Java内置锁(对象锁、隐式锁)。使用synchronized调用相对于获取syncObject的内置锁

2.1 synchronized同步方法

synchronized 关键字是 Java 的保留字,当使用 synchronized 关键字修饰一个方法的时候,synchronized位于返回类型前,该方法被声明为了同步方法

public class SynchronizedFunc implements Runnable{
    public Integer count=0;
	//临界代码区
    public synchronized void selfPlus(){
        count++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            selfPlus();
            System.out.println(Thread.currentThread().getName()+"count"+count);
        }

    }

    public static void main(String[] args) {
        SynchronizedFunc syn =new SynchronizedFunc();
        new Thread(syn).start();
        new Thread(syn).start();
    }
}

请添加图片描述

任何时间只允许一条线程进入同步方法(临界区代码段),如果其他线程都需要执行同一个方法,那么对不起,其他的线程只能等待和排队。

2.2synchronized同步代码块

为了执行效率,最好将同步方法分为小的临界代码段(减小锁粒度),通过代码块则得以解决

synchronized(syncObject) //同步块而不是方法
{
 //临界区代码段的代码块
}

syncObjec进入临界区代码段需要获取 syncObject 对象的监视锁(Java对象都有一把Monitor(监视锁))

   //之前的代码可以改写为
   @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            synchronized (this.sumLock){
                count++;
            }
         System.out.println(Thread.currentThread().getName()+"count"+count);
        }

    }

2.3synchronized 方法和 synchronized 同步块

  • 锁粒度不同

synchronized 方法是一种粗粒度的并发控制,某一时刻只能有一条线程执行该 synchronized 方法;synchronized 代码块则是一种细粒度的并发控制,处于 synchronized 块之外的其他代码,是可以被多条线程并发访问的。

  • 联系:在 Java 的内部实现上,synchronized方法实际上等同于用一个 synchronized 代码块
synchronized 代码块的括号中传入 this 关键字,使用 this 对象锁作为进入临界区的同步锁。
//版本一,使用 synchronized 代码块进行方法内部全部代码的保护,具体代码如下:
public void plus() {
 synchronized(this){ //进行方法内部全部代码的保护
 amount++; 
 } }

//版本二,synchronized 方法进行方法内部全部代码的保护,具体代码如下:
public synchronized void plus() {
 amount++; 
}

2.4 静态的同步方法

​ Java的对象有两种:Object实例对象和Class对象(类被加载到方法区时,都会为其创建一个Class对象,对于一个类来说,其Class对象也是唯一的)

​ 普通的 synchronized 实例方法,其同步锁是当前对象 this 的监视锁。那么,如果某个

synchronized 方法是 static 静态方法,而不是普通的对象实例方法,static的同步锁则是Class对象,比如下例的对象锁则为 StaticSync.class

public class StaticSync extends Thread{
    private static Integer amount=0;
    public static synchronized void selfPlus(){
        for (int i = 0; i < 10; i++) {
            amount++;
            System.out.println(Thread.currentThread().getName()+"---amount"+amount);
        }
    }

    @Override
    public void run() {
        selfPlus();
    }

    public static void main(String[] args) {
        new StaticSync().start();
        new StaticSync().start();
    }
}

请添加图片描述

使用 synchronized 关键字修饰 static 静态方法时,一个 JVM 内所有争用线程共用一把锁,是非常粗粒度的同步机制。但如果使用对象锁,并且 JVM 内的争用线程所争用的,是不同对象锁,则争用线程可以同步进入临界区,锁的粒度就变细;

总结:
1.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。
2.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得 当前实例 的锁
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁

2.5生产者与消费问题(版本一)

  • 临界资源:一个最多容纳10份烤鸭的盘子
  • 生产者:厨师每次往盘子放1份烤鸭(线程:Produce)
    • 临界代码区:放一份烤鸭的操作
  • 消费者:3位每次吃1分烤鸭的食客(线程:Consumer)
    • 临界代码区:拿一份烤鸭的操作

要保证临界资源被并发修改时的线程安全问题,通过Synchronized实现

public class Consumer implements Runnable{
    private Plate plate;

    public Consumer(Plate plate) {
        this.plate = plate;
    }


    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(80);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (plate){
                while(plate.getNum()==0){
                    try {
                        plate.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

            plate.setNum(plate.getNum()-1);
            System.out.println(Thread.currentThread().getName()+"---"+plate.getNum());

                plate.notifyAll();
            }

        }
    }
}
public class Produce implements Runnable{
    /**
     * 临界资源
     */
    private Plate plate;

    public Produce(Plate plate) {
        this.plate = plate;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (plate){
                while(plate.getNum()==10){
                    try {
                        plate.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

            plate.setNum(plate.getNum()+1);
            System.out.println(Thread.currentThread().getName()+"---"+plate.getNum());

                plate.notifyAll();
            }
        }
    }
}

public class Plate {
    private int num=0;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public static void main(String[] args) {
        Plate plate=new Plate();
        new Thread(new Consumer(plate)).start();
        new Thread(new Produce(plate)).start();
        new Thread(new Produce(plate)).start();
    }
}

3.线程间的通信(wait和notify)

3.1通知等待机制

通知/等待机制的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类Java.lang.Object上

wait();//调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。

notify();//通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁

notifyAll();//通知所有等待在该对象的线程

**注意:**wait、notify和notyfyAll要和synchronized一起使用,否则会报以下的错

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.mayikt.thread.days02.Thread03.run(Thread03.java:16)

3.2 Join/wait和sleep之间的区别

方法区别
sleep(long)线程在睡眠使不释放对象锁
join(long)join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait封装的,
wait(long)Wait(long)方法在等待的过程中释放对象锁 需要在我们synchronized中使用
3.2.1如何让三个线程T1,T2,T3按顺序执行
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1");
Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t2");
Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t3");
t1.start();
t2.start();
t3.start();

请添加图片描述

		Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t1");
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t2");
        Thread t3 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t3");
        t1.start();
        t2.start();
        t3.start();

请添加图片描述

使用join()可以实现3个线程顺序执行,原因分析:

t1,t2,t3并发执行,而执行t2的异步逻辑代码时遇到了t1.join。本质就是t2线程被 t1线程实例对象作为对象锁进行t1.wait()阻塞掉了,需要等待t1线程执行完成释放掉t1对象锁,t2线程获取t1的对象锁在进行执行

如果对上述描述不太理解,那我们就分析一下join的底层原理

		Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {

            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (t1){
                try {
                    t1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
//            try {
//                t1.join();
//            } catch (InterruptedException e) {
//
//            }
            System.out.println(Thread.currentThread().getName() + ",线程执行");
        }, "t2");

本质t2线程就是被 t1线程对象阻塞掉了即 t1.wait();
这个线程如何被唤醒的?唤醒的代码在jvm Hotspot 源码中 当jvm在关闭t1线程之前会检测阻塞
在t1线程对象上的线程,然后执行notfyAll(),这样t2线程就被唤醒了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值