深入理解并发编程之线程安全
文章目录
一、什么是线程安全问题?
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
通俗的说就是当多个线程同时去操作一个共享变量的时候,共享变量收到多个线程的干扰会发生不可思议的事情。
public class Thead001 extends Thread {
private static int count = 100;
@Override
public void run() {
while (count>0){
System.out.println(Thread.currentThread().getName() + ",正在出票");
count --;
System.out.println("剩余票数"+count);
}
}
public static void main(String[] args) {
Thread thread = new Thead001();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
new Thread(thread, "窗口4").start();
new Thread(thread, "窗口5").start();
}
}
看下图,窗口3和窗口1同时出售了第64张票,我们写的当count>0的时候才卖票,剩余票数为-2,完全超卖了。
二、怎么解决线程安全问题
核心思想: 在关键代码的地方只能同时有一个线程执行,用了锁,加锁的部分就变成了单线程。
1.使用Synchronized关键字
Synchronized是Java中的关键字,是一种同步锁,可以修饰类、方法、代码块。
public class Thead001 extends Thread {
private static int count = 100;
@Override
public synchronized void run() {
while (count>0){
System.out.println(Thread.currentThread().getName() + ",正在出票,第"+(100-count+1)+"张");
count --;
System.out.println("剩余票数"+count);
}
}
public static void main(String[] args) {
Thread thread = new Thead001();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
new Thread(thread, "窗口4").start();
new Thread(thread, "窗口5").start();
}
}
通过锁方法上面的示例就会变成这样,但是这样就不是多线程了,因为run()方法同一时刻只能一个线程执行,我们再优化下:
public class Thead001 extends Thread {
private static Integer count = 100;
@Override
public void run() {
/**
* ...这里可能还有其他操作
*/
synchronized(count){
while (count>0){
System.out.println(Thread.currentThread().getName() + ",正在出票,第"+(100-count+1)+"张");
count --;
System.out.println("剩余票数"+count);
}
}
}
public static void main(String[] args) {
Thread thread = new Thead001();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
new Thread(thread, "窗口4").start();
new Thread(thread, "窗口5").start();
}
}
假设线程内部还有其它操作的时候我们可以锁代码块,只锁票操作部分,这样只有票操作部分是单线程的,其它操作还是多线程。比上面稍微好一点,因为票操作地方还是需要等待执行的。
多线程安全的Map–ConcurrentHashMap,就是加了大量的synchronized来实现的。
2.使用Lock锁
ReentrantLock是JDK方法,需要手动声明上锁和释放锁,通过lock.lock()上锁,并且操作完成后通过lock.unlock()释放锁。注意使用lock锁需要try/catch 避免内部代码报错导致锁未释放
public class Thread007 extends Thread {
private static int count = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
/**
* ...这里可能还有其他操作
*/
try {
lock.lock();
while (count>0){
System.out.println(Thread.currentThread().getName() + ",正在出票,第"+(100-count+1)+"张");
count --;
System.out.println("剩余票数"+count);
}
}catch (Exception e){
}finally {
// 释放锁
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread = new Thread007();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
new Thread(thread, "窗口4").start();
new Thread(thread, "窗口5").start();
}
}
lock锁和synchronized锁之间的区别:
- Synchronized属于java内置的关键字,而lock锁是基于AQS封装的一个锁的框架。
- Synchronized当代码执行结束自动释放锁,而lock需要人工释放锁,相对于来说lock锁更加灵活
3.使用CAS无锁机制
CAS(compare and swap)机制是先比较再交换,这种机制并不是给代码上了锁,而是操作的时候先对数据进行了比较,如果数据符合预期,再进行数据处理,这是一种乐观锁,举个例子:比如我们数据库表一般会建有一个version字段,每次修改数据version++,每次需要修改的时候先对比version字段是不是跟取出来的时候一样,如果是就修改,如果不是说明别人动过数据反馈给用户不做处理。
Java中AtomicInteger就是通过CAS原理实现的。
public class Thread007{
static AtomicInteger count = new AtomicInteger(1000);
static int count1 = 1000;
public static void main(String[] args) {
//这个是计数器,用来等待10个线程都执行结束
final CountDownLatch latch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
for (int j = 1; j <= 100; j++) {
count.decrementAndGet();
count1--;
}
//当前一个线程执行结束计数器减一
latch.countDown();
}).start();
}
try {
//这里会一直等待所有线程执行完,latch为0才会放行
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
System.out.println(count1);
}
}
上面代码执行的结果为,直接通过int操作的每次的结果不固定,而通过AtomicInteger 执行的结果固定为正确结果0,因为AtomicInteger 的加减是安全的,i++或i–操作是线程不安全的。
三、死锁
1.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个例子:在没有红绿灯的十字路口,假设车道宽度之允许一个车通过,A车从东往西行驶,B车从南往北行驶,恰巧都走到了最中间,然后两边都想过去却互不相让卡主的状态就是死锁。
看下面代码:
public class Thread004 implements Runnable {
private Object objectB = new Object();
private Object objectA = new Object();
private static boolean flag = true;
@Override
public void run() {
if (flag) {
while (true){
synchronized (objectA) { // 获得lockA的锁
System.out.println(Thread.currentThread().getName() + "A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
synchronized (objectB) { // 获得lockB的锁
System.out.println(Thread.currentThread().getName() + "B");
} // 释放lockB的锁
} // 释放lockA的锁
}
} else {
while (true) {
synchronized (objectB) { // 获得lockB的锁
System.out.println(Thread.currentThread().getName() + "A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
synchronized (objectA) { // 获得lockA的锁
System.out.println(Thread.currentThread().getName() + "B");
} // 释放lockA的锁
} // 释放lockB的锁
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread004 thread004 = new Thread004();
new Thread(thread004, "线程1").start();
Thread.sleep(10);
thread004.flag = false;
new Thread(thread004, "线程2").start();
}
}
看代码执行的结果:先创建了线程1,此时flag为True线程1执行true,手里持有的锁标记为objectA,里面代码执行需要所标记objectB,然后flag修改为false,线程2直接持有所标记objectB ,线程2手里持有方法执行需要锁标记objectA。这个时候线程1和线程2分别持有对方锁需要的所标记,就形成了死锁。
2.如何排查死锁
查找死锁可以使用JDK本身自带工具jconsole.exe。
工具地址:在所安装JDK的bin目录下
C:\Program Files\Java\jdk1.8.0_301\bin
看下图,下面有检测死锁的功能,自动锁定死锁的线程。线程1在阻塞状态,等待所标记被线程2持有。死锁位置为Thread004第27行
同样查看线程2也是在阻塞状态,等待的所标记被线程1持有。死锁位置为Thread004第40行
3.怎么预防死锁
- 避免一个线程同时获取多个锁,也就是避免一个线程在手里有锁的情况下去获取第二把锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用Lock.tryLock(timeout)来替代使用内部锁机制,当锁超时后会自动释放锁标记。