闭关修炼(二)线程安全问题

?回归初心



什么是线程安全问题?

当多个线程共享同一个全局变量,做写的时候(如count++),可能会受到其他线程的干扰,导致数据有问题,这种现象叫做线程安全问题。

做读操作的时候,不会导致线程安全问题。

局部变量会发生线程安全问题吗?

多个线程共享一个局部变量,做写的操作,会发生线程安全问题吗?
不会,局部变量的定义是run方法中的变量,局部变量不是全局的,每个线程里的局部变量不会影响其他线程中的局部变量。

什么是线程之间的同步?

这里的同步和之前的同步异步不是同一个概念, 线程之间的同步的意思是保证数据的原子性,即数据不能受到其他线程的干扰。

线程不安全例子

两个窗口抢票案例:

package duoxiancheng;
import lombok.SneakyThrows;

class ChuangKouThread implements Runnable {
    private int trainCount = 100;

    @SneakyThrows
    public void run() {
        while (trainCount > 0){
            Thread.sleep(50);
            sale();
        }
    }

    public void sale() {
        System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
        trainCount--;
    }
}

public class Qiangpiao {
    public static void main(String[] args) {
        ChuangKouThread chuangKouThread = new ChuangKouThread();
        Thread  t1 = new Thread(chuangKouThread, "窗口1");
        Thread  t2 = new Thread(chuangKouThread, "窗口2");
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

线程安全问题有什么解决办法?

解决办法有哪些?

  1. synchronized
  2. lock — jdk1.5包

lock和synchronized有什么区别?
synchronized自动设置锁开关,lock手动设置

使用同步代码块解决线程安全问题

什么地方需要加锁?

真正操作共享全局变量时加锁

线程安全问题的解决的基本思想?

定义对象锁,使用synchronized代码块包裹需要解决线程安全问题的地方。synchronized代码块判断谁先拿到对象锁,如果已有线程在使用了,还未被释放时 ,线程将进行等待,直到对象锁被释放,再执行代码块的内容。

	private int trainCount = 100;

    @SneakyThrows
    public void run() {
        while (trainCount > 0){
            Thread.sleep(50);
            sale();
        }
    }
	private Object mutex = new Object();
    public void sale() {
        synchronized (mutex){
            if (trainCount > 0){
                System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
                trainCount--;
            }
        }
    }

上面这段代码,如果把sale方法中判断票数是否大于0的逻辑判断代码注释掉,会出现第101张票,为什么?
线程等待争抢最后一张票时,若不加逻辑判断就会产生第101张票。

使用同步代码块有哪些条件?

  1. 至少有两个线程以上,需要发生同步
  2. 多个线程想要同步,必须要用同一把锁
  3. 同步代码块中应保证只有一个线程执行

线程同步的原理?

  1. 有一个线程已经拿到锁,其他线程获得cpu执行权时也不会执行同步代码块,等待线程释放锁。

  2. 代码执行完毕或者是程序抛出异常时,锁会被释放掉

  3. 如果当前线程获取锁,则执行同步代码块。

线程同步有什么缺点?

  1. 效率非常低,多个线程需要判断锁,消耗大量资源。
  2. 线程间会争抢锁的资源。
  3. 如果有线程不释放锁,则会产生死锁问题

使用同步函数解决线程安全问题

什么是同步函数?

在方法上修饰synchronized的函数称为同步函数

例子:

	public synchronized void sale() {
        if (trainCount > 0){
            System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
            trainCount--;
        }
    }

同步函数使用的是什么锁?如何证明?

this锁。
两个线程之间实现同步,一个线程使用this锁同步代码块,另一个线程使用同步函数,如果这两个线程同步,说明同步函数使用的是this锁。把this锁换成Object锁,则出现重复数据

测试代码:

class ChuangKouThread implements Runnable {
    private int trainCount = 100;
    private boolean flag;

    @SneakyThrows
    public void run() {
        String name = Thread.currentThread().getName();
        flag = name.equals("窗口1");

        if (flag) {
            while (trainCount > 0) {
                Thread.sleep(50);
                sale();
            }
        } else {
            while (trainCount > 0) {
                Thread.sleep(50);
                synchronized (this) {
                    if (trainCount > 0) {
                        System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
                        trainCount--;
                    }
                }

            }
        }

    }

    public synchronized void sale() {
        if (trainCount > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
            trainCount--;
        }
    }
}
public class Qiangpiao {
    public static void main(String[] args) {
        ChuangKouThread chuangKouThread = new ChuangKouThread();
        Thread t1 = new Thread(chuangKouThread, "窗口1");
        Thread t2 = new Thread(chuangKouThread, "窗口2");
        t1.start();
        t2.start();
    }
}

所以,一个线程使用同步函数,另一个线程使用this锁,可以线程同步。

所以,一个线程使用同步函数,另一个线程使用同步代码块,如果是非this锁,不能线程同步。

为什么同步函数设计用this锁?

因为底层设计觉得这样更合理一些(雾)

使用静态同步函数解决线程安全问题

什么是静态同步函数?

在方法上加synchronized称为同步函数,同步函数又分为两种,非静态和静态。
同步函数使用的是this锁。
非静态同步函数,有static关键字进行修饰的synchronized。

静态同步函数使用的是什么锁?

不使用this锁,静态方法里面不可能有this,使用的是当前的字节码文件。
当一个变量被static修饰时,这个变量存放永久区,当class文件被加载时就会被初始化。

java中字节码文件用类名.class定义

证明静态同步函数使用的是字节码文件:

class ChuangKouThread implements Runnable {
    private static int trainCount = 100;
    private boolean flag;

    Object mutex = new Object();
    @SneakyThrows
    public void run() {
        String name = Thread.currentThread().getName();
        flag = name.equals("窗口1");

        if (flag) {
            while (trainCount > 0) {
                Thread.sleep(50);
                sale();
            }
        } else {
            while (trainCount > 0) {
                Thread.sleep(50);
                synchronized (ChuangKouThread.class) {
                    if (trainCount > 0) {
                        System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
                        trainCount--;
                    }
                }


            }
        }

    }

    public static synchronized void sale() {
        if (trainCount > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
            trainCount--;
        }
    }
}

注意事项

线程同步的共享全局变量是静态还是非静态,和是同步函数还是非同步函数没有关系。

小试牛刀题

两个线程,一个线程使用同步函数,另一个线程使用静态同步函数能实现同步吗?
不能,因为同步函数使用this锁,静态同步函数使用当前字节码文件。

加锁和同步的概念

加锁保证同步,同步是保证数据安全和原子问题。

目前内容和分布式锁、高并发和jvm锁没有任何关系

多线程死锁

什么是多线程死锁?

同步中嵌套同步,导致锁无法释放,程序卡死。

演示例子

class ChuangKouThread implements Runnable {
    private int trainCount = 100;
    private boolean flag;

    Object mutex = new Object();

    @SneakyThrows
    public void run() {
        String name = Thread.currentThread().getName();
        flag = name.equals("窗口1");

        if (flag) {
            while (true) {
                synchronized (mutex) {
                    sale();
                }
            }
        } else {
            while (true) {
                sale();
            }
        }

    }

    @SneakyThrows
    public synchronized void sale() {
        synchronized (mutex) {
            if (trainCount > 0) {
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - trainCount + 1) + "张票");
                trainCount--;
            }
        }

    }
}

死锁过程分析

线程1先拿到同步代码块mutex锁,要拿到同步函数的this锁时,被线程2先拿到同步函数的this锁,等待
线程2先拿到同步函数的this锁,要拿到同步代码块mutex锁时,被线程1先拿到同步代码块的mutex锁,等待

这个过程即同步中嵌套同步,互相不释放

多线程的三大特性

三大特性有哪些?

  1. 原子性
    保证线程独一无二,目的是保证线程安全
  2. 可见性
    java内存模型(见下)
  3. 有序性
    使用join wait notify方法控制线程的有序进行,主要运用在多线程之间通讯

JAVA内存模型

什么是java内存模型?

java内存模型和java内存结构不是一回事,java内存模型属于多线程可见性jmm,java内存结构属于jvm内存分配。要注意区分!

java内存模型决定了一个线程与另一个线程是否可见。

java内存模型中主内存主要存放共享的全局变量,而私有本地内存主要存放本地线程私有变量。

共享全局变量如Count存放于主内存,多个线程可以对其操作,在线程内部又存在有本地私有内存。
在这里插入图片描述
线程要对共享数据进行写操作时,线程要先从主内存中拷贝一份放在自己的本地内存中,再进行写操作,线程不安全问题产生于此。

具体地,t1和t2同时对count进行写的操作,不会在主内存进行写操作,而是在本地内存中对副本做写操作,而数据副本被t1和t2同时取出,其值都为0, 假定线程都进行写操作count++,更新完副本数据后再将数据刷新到主内存里面去,主内存count值更改了两次,其值都为1。

但如果t1和t2并不是同时进行写操作,主内存能进行通知,保证线程的安全,具体地,t1将count值刷新到主内存后,主内存会立刻通知t2的本地内存,这时,t2的本地内存中count的副本改为新的值,这样就保证了一定的线程的安全。

所以主内存通知线程来不来得及,就是否产生线程安全问题。如果主内存通知其他线程不及时,就会产生数据的冲突。

以上就是java内存模型。

可见性-Volatile关键字

Volatile关键字的作用?

让变量在多个线程之间可见,即本地内存的值发生改变后立马通知给另一个线程。

Volatile关键字的缺点?

不保证原子性,即不保证线程安全性问题。

代码样例

class ThreadValatile extends Thread {
    public boolean flag = true;
	//public volatile boolean flag = true;
	
    public void run() {
        System.out.println("子线程开始执行");
        while (flag) {
        }
        System.out.println("子线程结束执行");
    }

    public void setFlag(boolean bool) {
        flag = bool;
    }
}

public class Vali {
    @SneakyThrows
    public static void main(String[] args) {
        ThreadValatile thread = new ThreadValatile();
        thread.start();
        Thread.sleep(3000);
        thread.setFlag(false);
        System.out.println("Flag值修改为false");
        Thread.sleep(1000);
        System.out.println(thread.flag);
    }
}

在线程运行时,主内存立的Flag为true,线程将flag拷贝了一份副本到自己的本地内存中;
主线程修改了共享的全局变量Flag,值修改为false,即主内存的值改成false了,但是代码仍然会在死循环,说明本地内存的值仍然为true,这时候flag加上volatile关键字,一但主内存flag值被修改,则立马通知其他的子线程并子线程的本地内存与主内存同步。

原子类-AtomicInteger

非原子性样例

class ThreadValatile extends Thread {
    public int count = 0;

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

}

public class Vali {
    @SneakyThrows
    public static void main(String[] args) {
        ThreadValatile[] threadList = new ThreadValatile[10];
        for (int i = 0; i < threadList.length; i++) {
            threadList[i]=new ThreadValatile();
            threadList[i].start();
        }

    }
}

这段代码结果:
在这里插入图片描述
都是1000,相当每个线程只都对自己的局部变量进行修改,并没有共享数据。想要共享count数据,则在count前加上volatile static关键字,static数据只存放一次在静态区中,所有线程都会共享,并即时通知其他线程。

将count添加volatile static关键字后,再多次执行,发现会产生不一样的结果,并且count不等于10000:
在这里插入图片描述
所以这也说明了volatile关键字并不能保证数据的原子性。

AtomicInteger样例

class ThreadValatile extends Thread {
    public static AtomicInteger count = new AtomicInteger();
    public void run() {
        for (int i = 0; i < 1000; i++) {
            // count++
            count.incrementAndGet();
        }
        System.out.println(Thread.currentThread().getName() + ": " + count.get());
    }

}

public class Vali {
    @SneakyThrows
    public static void main(String[] args) {

        ThreadValatile[] threadList = new ThreadValatile[10];
        for (int i = 0; i < threadList.length; i++) {
            threadList[i] = new ThreadValatile();
            threadList[i].start();
        }

    }
}

其他Atomic的API

自行查阅java的API手册
我是在这里看的https://www.matools.com/api/java8
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值