一、为什么会存在多线程安全问题?
当多个线程同时共享同一个全局变量或者静态变量,做写的操作时,可能会发生数据冲突问题。但是做读的操作是不会发生数据冲突问题。
举一个常见的例子火车票卖票问题:
package com.heyu.thread.demo;
/**
* @author: _无邪
* @date: 2021-02-22 9:25
* @Description: 模拟订票
* <p>
* synchronized 是自动锁 代码运行完毕 或 抛出异常 会自动释放锁
* lock 人为控制 获取 、释放锁, 扩展性高
*/
public class Thread01 implements Runnable {
private int count = 100;
// private ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread01 threadControllerTest = new Thread01();
new Thread(threadControllerTest, "线程1").start();
new Thread(threadControllerTest, "线程2").start();
}
public void ticket() {
if (count != 0) {
count--;
System.out.println(Thread.currentThread().getName() + ":" + "这是第" + (100 - count) + "张票:");
}
}
@Override
public void run() {
while (count > 0) {
ticket();
}
}
}
运行结果:
由此可看出在不加锁的情况下,会出现车票重复出售的情况。
解决方法添加this锁,就会避免车票重复出售的情况。如下代码所示:
package com.heyu.thread.demo;
/**
* @author: _无邪
* @date: 2021-02-22 9:25
* @Description: 模拟订票
* <p>
* synchronized 是自动锁 代码运行完毕 或 抛出异常 会自动释放锁
* lock 人为控制 获取 、释放锁, 扩展性高
*/
public class Thread01 implements Runnable {
private int count = 100;
// private ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread01 threadControllerTest = new Thread01();
new Thread(threadControllerTest, "线程1").start();
new Thread(threadControllerTest, "线程2").start();
}
public synchronized void ticket() {
if (count != 0) {
count--;
System.out.println(Thread.currentThread().getName() + ":" + "这是第" + (100 - count) + "张票:");
}
}
@Override
public void run() {
while (count > 0) {
ticket();
}
}
}
二、常见线程安全解决方法:
1、内置的锁
Java提供了一种内置的锁机制来支持原子性
每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁
内置锁使用synchronized关键字实现,synchronized关键字有两种用法:
1.修饰需要进行同步的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象
2.同步代码块和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活
2、同步代码块synchronized
就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(锁对象){
可能会发生线程冲突问题代码
}
- 锁对象 任意对象
- 必须保证多个线程使用的是同一个锁对象
- 把{} 只让一个线程进
案例:
package com.heyu.thread.thread;
/**
* @author: _无邪
* @date: 2021-02-23 11:23
* @Description: 线程通信
* 负载均衡算法 轮询
* count = (count + 1) % 2;
*/
public class Thread04 {
private Res res = new Res();
class Res {
private String username;
private String sex;
private boolean flag ;
}
class writeOper implements Runnable {
public int count = 0;
public Res res;
public writeOper(Res res) {
this.res = res;
}
/**
* 使用object锁,两个对象共享共一个res ,必须使用同一个object锁
*/
@Override
public void run() {
while (true) {
synchronized (res) {
if(res.flag){
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (count == 0) {
res.username = "_无邪";
res.sex = "男";
} else {
res.username = "小小";
res.sex = "女";
}
// 负载均衡算法 轮询
count = (count + 1) % 2;
res.flag = true;
res.notify();
}
}
}
}
class readOper implements Runnable {
private Res res;
public readOper(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
if(!res.flag){
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(res.username + "," + res.sex);
res.flag = false;
res.notify();
}
}
}
}
public void start() {
Res res = new Res();
writeOper writeOper = new writeOper(res);
readOper readOper = new readOper(res);
new Thread(writeOper).start();
new Thread(readOper).start();
}
public static void main(String[] args) {
new Thread04().start();
}
}
3、同步方法
- 创建一个方法 修饰符添加synchronized
- 把访问了 共享数据的代码放入到方法中
- 调用同步方法
案例:
package com.heyu.thread.thread;
/**
* @author: _无邪
* @date: 2021-02-22 9:25
* @Description: 模拟订票
* <p>
* synchronized 是自动锁 代码运行完毕 或 抛出异常 会自动释放锁
* lock 人为控制 获取 、释放锁, 扩展性高
*/
public class Thread01 implements Runnable {
private int count = 100;
// private ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread01 threadControllerTest = new Thread01();
new Thread(threadControllerTest, "线程1").start();
new Thread(threadControllerTest, "线程2").start();
}
public synchronized void ticket() {
if (count != 0) {
count--;
System.out.println(Thread.currentThread().getName() + ":" + "这是第" + (100 - count) + "张票:");
}
}
@Override
public void run() {
while (count > 0) {
ticket();
}
}
}
三、多线程死锁
锁循环嵌套,导致锁无法释放
案例:
package com.heyu.thread.thread;
/**
* @author: _无邪
* @date: 2021-02-22 9:25
* @Description: 死锁
* <p>
* 线程1 先调用OBJECT锁 在调用 this锁
* 线程二先调用this锁 在调用OBJECT锁
* 线程 锁 循环嵌套 导致死锁
*/
public class Thread02 implements Runnable {
private int count = 100;
private boolean flag = true;
private Object object = new Object();
public static void main(String[] args) {
Thread02 threadTest02 = new Thread02();
new Thread(threadTest02, "线程1").start();
try {
Thread.sleep(40);
} catch (InterruptedException e) {
}
threadTest02.flag = false;
new Thread(threadTest02, "线程2").start();
}
private synchronized void ticket() {
synchronized (object) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
if (count != 0) {
count--;
System.out.println(Thread.currentThread().getName() + ":" + "这是第" + (100 - count) + "张票:");
}
}
}
@Override
public void run() {
if (flag) {
while (count > 0) {
synchronized (object) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
ticket();
}
}
} else {
while (count > 0) {
ticket();
}
}
}
}
运行结果:
可用jconsole检测死锁,并查看死锁位置,jconsole使用方法可百度,这边不做介绍。
jconsole检测结果如下所示:
四、多线程三大特性
1、原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分,
2、可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
3、有序性
程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
五、volatile 关键字
volatile:可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。禁止指令重排序。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。
这边采用单例模式中的恶汉模式做介绍:
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
如果不加volatile 会造成的结果:
当有两个线程A和B同时进入if方法,A首先进入对象锁,因为对象为空,所以执行实例化方法,由于jvm的内部优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。当B进入的时候发现对象部位null所以不实例化,此时当B线程调用对象实例时就会发现他并没有被初始化,就会发生错误!!!
Volatile与Synchronized区别
- volatile虽然具有可见性但是不能保证原子性。
- 性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
- 但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。