线程安全
- 如果你的代码在
多线程下
执行和在单线程下
执行永远都能获得一样的结果
,那么你的代码就是线程安全的
线程安全级别
1、不可变
像
String、Integer、Long
这些,都是final类型
的类,任何一个线程都改变不了它们的值
,要改变除非新创建一个
,因此这些不可变对象不需要任何同步手段
就可以直接在多线程环境下使用
2、
绝对
线程安全不管运行时环境如何,调用者
都不需要额外的同步措施
。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的
。不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList
、CopyOnWriteArraySet
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方式实现:
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就会永远等待下去,产生最简单的死锁。 一个类可能发生死锁,并不意味着每次都会发生,往往在高并发、高负载的情况下,死锁出现概率高很多。
多线程同步的时候, 如果
同步代码嵌套, 使用相同锁
, 就有可能出现死锁
写一个死锁程序
哲学家进餐
问题,使用同步代码块嵌套
,互相先持有对方需要的锁对象
写一个死锁程序步骤:
- 定义两个对象分别代表两个线程一开始就持有的锁对象
- 在run方法中使用 synchronized 同步代码块嵌套
外层synchronized锁对象
是对方所需求的
,自己所持有的
,内层synchronized锁对象
是对方所持有
,自己所需要的
。- 当一个线程中的锁对象是自己持有的,还未走出外层代码块,
需要对方所持有的锁对象时
,cpu调度到了另一个线程
,另一个线程正好也是这种情况,此时双方都持有了对方所需要的锁对象,发生了死锁。
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 执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。 */ } }
结果:
上方结果省略.... Thread-1--持有了left,想得到right Thread-0--持有了right,想得到left
执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。
避免死锁的方式
- 注意和减少同步代码块嵌套问题
- 设计时考虑清楚锁的顺序,尽量
减少嵌套加锁交互数量
- 由于死锁是因为两个或多个线程之间无限时间等待对方持有的锁对象而形成的,那么给同步代码块加个等待时间限制。
- synchronized 关键字 不具备这个功能,使用
Lock类中的tryLock方法
,指定一个超时时限
,在等待时,若超过该时限,就返回一个失败信息结束阻塞。
- synchronized 关键字 不具备这个功能,使用
单例模式的线程安全问题
单例模式
- 单例设计模式:保证一个类在内存中只有一个对象,内存唯一。
- 保证类在内存中只有一个对象:
- 1、控制类的创建,不让其他类来创建本类的对象,将本类的构造函数私有private
- 2、在本类中定义一个本类的对象,并且外界无法修改。
- 3、在本类中提供一个唯一的公共访问方法,可获取本类的对象。
饿汉式-线程安全
- 在类中直接创建一个不可修改的对象引用,
不管有没有调用,都创建,空间换时间
。 - 饿汉式在多线程环境下是
线程安全
的。
class Singleton {
//1.将本类的构造函数私有private
private Singleton (){}
//2. 在本类中定义一个本类的对象,并且外界无法修改。
private static Singleton s = new Singleton();
//3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
//饿汉式
public static Singleton getInstance(){
return s ;
}
}
另一种饿汉式,利用final
直接修饰
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了两次,就不是唯一的内存引用了。
class Singleton {
//1.将本类的构造函数私有private
private Singleton (){}
//2. 在本类中定义一个本类的对象,并且外界无法修改。
private static Singleton s ;
//3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
//懒汉式 对象引用为空 才创建,
public static Singleton getInstance(){
//用到时创建,用不到时不创建
if(s == null)
s = new Singleton() ;
return s;
}
}
饿汉式和懒汉式的区别
- 线程安全上:
- 饿汉式线程安全,多线程下也不会创建多个对象
- 懒汉式非线程安全,多线程下可能会创建多个对象
- 执行效果:
- 饿汉式是 空间换时间,执行速度快。
- 懒汉式是 时间换空间,延迟加载。