Java多线程基础-线程安全和死锁问题

线程安全

  • 如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的

线程安全级别

  • 1、不可变

    String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

  • 2、绝对线程安全

    不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的。不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayListCopyOnWriteArraySet

  • 3、相对线程安全

    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制

  • 4、 线程非安全

    ArrayList、LinkedList、HashMap等都是线程非安全的类

常见的线程安全类

线程安全类 线程不安全类
Vector ArrayList
StringBuffer StringBuilder
Hashtable HashMap
LinkedList
  • StringBuffer 线程安全(其append方法中加了synchronized修饰
  • vector add、remove方法都是原子操作,加了synchronized修饰
  • 但是Collections集合工具类中提供了静态方法synchronizedXXX(XXX),分别对应着线程不安全的那些集合类,可以让他们转换成线程安全的集合,所以Vector类淘汰了…
方法摘要 方法说明
static <T> Collection<T> synchronizedCollection(Collection<T> c) 返回指定 collection 支持的同步(线程安全的)collection。
static <T> List<T> synchronizedList(List<T> list) 返回指定列表支持的同步(线程安全的)列表。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全的)映射。
static <T> Set<T> synchronizedSet(Set<T> s) 返回指定 set 支持的同步(线程安全的)set。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) 返回指定有序映射支持的同步(线程安全的)有序映射。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) 返回指定有序 set 支持的同步(线程安全的)有序 set。

多线程中的线程安全问题

  • 多线程并发操作同一共享数据时,就会可能出现线程安全问题。

  • 使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 就不会多个线程同时执行

多窗口卖票问题

  • 如果不开启锁同步 ,就会出现卖出票号为负数的现象
  • 在循环中使用锁同步,让各个线程进入循环之后进行同步,一个线程把ticketNum–后,其他线程再执行
  • 使用Runnable方式实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class SynchronizeTicketTest {

    public static void main(String[] args) {

    new Thread(new TicketSeller()).start();
    new Thread(new TicketSeller()).start();
    new Thread(new TicketSeller()).start();
    new Thread(new TicketSeller()).start();

    }
    }

    class TicketSeller implements Runnable{
    private static int tikcetNum = 10000;//总共10000张票,放到静态池中共享
    @Override
    public void run() {
    while(true){
    //在循环中使用锁同步,让各个线程进入循环之后进行同步,一个线程把ticketNum--后,其他线程再执行
    synchronized(TicketSeller.class){
    if(tikcetNum <= 0)
    break;
    try {
    //让线程睡10ms 如果不开启锁同步 就会出现票号为负数的现象
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "...这是第" + tikcetNum-- + "号票");

    }
    }
    }
    }

死锁问题

  • 线程A和线程B相互等待对方持有的锁导致程序无限死循环下去
  • 线程A持有锁H并且想获取锁W,此时线程B持有锁W并且想获取锁H,那么这两个线程AB就会永远等待下去,产生最简单的死锁。
  • 一个类可能发生死锁,并不意味着每次都会发生,往往在高并发、高负载的情况下,死锁出现概率高很多。

  • 多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁

写一个死锁程序

  • 哲学家进餐问题,使用同步代码块嵌套,互相先持有对方需要的锁对象

  • 写一个死锁程序步骤:

    1. 定义两个对象分别代表两个线程一开始就持有的锁对象
    2. 在run方法中使用 synchronized 同步代码块嵌套
    3. 外层synchronized锁对象对方所需求的自己所持有的内层synchronized锁对象对方所持有自己所需要的
    4. 当一个线程中的锁对象是自己持有的,还未走出外层代码块,需要对方所持有的锁对象时,cpu调度到了另一个线程,另一个线程正好也是这种情况,此时双方都持有了对方所需要的锁对象,发生了死锁。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class DeadLockTest {
    private static String left = "left one";
    private static String right = "right one";
    public static void main(String[] args) {

    new Thread(() -> {
    while(true){
    synchronized (right){
    System.out.println(Thread.currentThread().getName()+"--持有了right,想得到left");
    synchronized(left){
    System.out.println(Thread.currentThread().getName()+"--得到了left,可以开吃了");
    }
    }
    }
    }).start();

    new Thread(() -> {
    while(true){
    synchronized (left){
    System.out.println(Thread.currentThread().getName()+"--持有了left,想得到right");
    synchronized(right){
    System.out.println(Thread.currentThread().getName()+"--得到了right,可以开吃了");
    }
    }
    }
    }).start();

    /*
    Thread-1--持有了left,想得到right
    Thread-0--持有了right,想得到left
    执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。
    */

    }
    }

    结果:

    1
    2
    3
    上方结果省略....
    Thread-1--持有了left,想得到right
    Thread-0--持有了right,想得到left

    执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。

避免死锁的方式

  • 注意和减少同步代码块嵌套问题
  • 设计时考虑清楚锁的顺序,尽量减少嵌套加锁交互数量
  • 由于死锁是因为两个或多个线程之间无限时间等待对方持有的锁对象而形成的,那么给同步代码块加个等待时间限制。
    • synchronized 关键字 不具备这个功能,使用Lock类中的tryLock方法,指定一个超时时限,在等待时,若超过该时限,就返回一个失败信息结束阻塞。

单例模式的线程安全问题

单例模式

  • 单例设计模式:保证一个类在内存中只有一个对象,内存唯一。
  • 保证类在内存中只有一个对象:
    • 1、控制类的创建,不让其他类来创建本类的对象,将本类的构造函数私有private
    • 2、在本类中定义一个本类的对象,并且外界无法修改。
    • 3、在本类中提供一个唯一的公共访问方法,可获取本类的对象。

饿汉式-线程安全

  • 在类中直接创建一个不可修改的对象引用,不管有没有调用,都创建,空间换时间
  • 饿汉式在多线程环境下是线程安全的。
1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
//1.将本类的构造函数私有private
private Singleton (){}
//2. 在本类中定义一个本类的对象,并且外界无法修改。
private static Singleton s = new Singleton();

//3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
//饿汉式
public static Singleton getInstance(){
return s ;
}
}

另一种饿汉式,利用final直接修饰

1
2
3
4
5
6
7
class Singleton {
//1.将本类的构造函数私有private
private Singleton (){}
//2. 在本类中定义一个本类的对象,并且外界无法修改。
public final static Singleton s = new Singleton() ;

}

懒汉式-非线程安全

  • 在类中获取对象时加以判断,为空时才创建,即用到该类对象时才创建,时间换空间。
  • 懒汉式单例模式在多线程下是非线程安全的。
    • 当线程A判断为null时,正准备new,此时,被另一个线程B抢占了CPU资源,线程B也判断为null,new了之后,第一个线程A又抢回了CPU资源,此时线程A又new了。此时这两个线程就new了两次,就不是唯一的内存引用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
//1.将本类的构造函数私有private
private Singleton (){}
//2. 在本类中定义一个本类的对象,并且外界无法修改。
private static Singleton s ;
//3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
//懒汉式 对象引用为空 才创建,
public static Singleton getInstance(){
//用到时创建,用不到时不创建
if(s == null)
s = new Singleton() ;
return s;
}
}

饿汉式和懒汉式的区别

  • 线程安全上:
    • 饿汉式线程安全,多线程下也不会创建多个对象
    • 懒汉式非线程安全,多线程下可能会创建多个对象
  • 执行效果:
    • 饿汉式是 空间换时间,执行速度快。
    • 懒汉式是 时间换空间,延迟加载。