1:线程安全
一:什么是线程安全性
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的.----《并发编程实战》
二:什么是线程不安全
多线程并发访问时,得不到正确的结果.
从字节码角度剖析线程不安全操作
举例:
前置操作:
- javac -encoding UTF-8 UnsafeThread.java 编译成.class
- javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令
0: getstatic #2 //获取指定类的静态域,并将其押入栈顶
3: iconst_1 //将int型1押入栈顶
4: iadd //将栈顶两个int型相加,将结果押入栈顶
5: putstatic #2 //为指定类静态域赋值
8: return
例子中,产生线程不安全问题的原因:
num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性.
2:锁的分类
锁的分类
-
自旋锁: 在获取自旋锁的时候,先必须先得到锁,在没有线程获取到当前锁的时候,就可以获取到锁直接执行锁内的代码,在有其他线程持有当前锁的时候,则当前线程置为挂起状态,等待其他线程执行完成释放锁后再执行,该锁一些特定情况下容易导致死锁,例如在递归过程中,在第二次调用则会因无法获取到锁,进而导致进入死锁状态
-
阻塞锁: 例如java中的synchronize的wait,notify,notifyAll,当wait的时候,当前线程挂起,当notify,notifyAll获得相应的信号时,当前线程就可以继续执行,其继续执行依据遵循liunx的线程任务调度机制
-
重入锁: 例如java中的synchronize修饰的方法,当前线程获取到该锁后,可以无限制调用该方法,其他线程需要等到该线程释放锁后才能得以执行
-
读写锁: 假如有两个线程,同时执行写,同一时刻只能有一个线程执行,一个写一个读,同一时刻只能有一个执行必须有先后,都是读的话就没有限制
-
互斥锁: 例如java中的synchronize,Lock锁的持有者有且最多只有一个,必须当锁的持有者释放之后才能够被其他线程持有
-
悲观锁: 例如java中的synchronize,Lock,按其名称的理解,就是对于锁内内容的访问都视为将会修改数据,从而限定每次锁的持有者只能有一个
-
乐观锁: 按其名称的理解,对该锁的持有者不作限定,同时其依旧遵从于读写锁的原则设定
-
公平锁: 获取锁的持有,按照队列顺序依次获取
-
非公平锁: 在获取锁的队列中可进行插队,随机抽取队列单元给于锁等操作
-
偏向锁->轻量级锁->重量级锁: 依据当前的锁的竞争激烈情况而进行相当的升级
-
独占锁: 例如synchronize,Lock,这也只是一种概念,独占锁模式下,每次只能有一个线程能持有锁
-
共享锁: 允许多个线程共享该锁,但只能读数据,不能修改数据
3.实现线程安全:synchronize,lock
一:synchronize使用解析:
- synchronize修饰普通方法 : 锁住对象的实例,并不会锁住该对象所在类的整个类.
public synchronized void create() {}
- synchronize修饰静态方法 : 锁住该对象所在类的整个类.
public static synchronized void create() {}
- synchronize修饰代码块 :
- 成员锁:锁住变量,并不会锁住该对象所在类的整个类.
- 实例对象锁: 锁住对象的实例,并不会锁住该对象所在类的整个类.
- 当前类的 class 对象锁: 锁住该对象所在类的整个类.
//成员锁
public Object create(Object o) {
synchronized(o) {
// 操作
}
}
//对象锁
synchronized(this) {
for (int j = 0; j < 100; j++) {
i++;
}
}
// class 类锁
synchronized(AccountingSync.class) {
for (int j = 0; j < 100; j++) {
i++;
}
}
总结:如果作用的是非静态的方法或者代码块,它取得的锁是一个对象,如果作用的是静态代码块,它取得的锁是这个类,这个类中所有对象是同一把锁
二:Lock与synchronized的区别
操作不同点:
- lock获取锁与释放锁的过程都需要手动
- synchronize都是自动的
原理机制不同点:
- Lock 依赖于CPU指令,java代码
- synchronize依赖于jvm
4.线程间的通信
一:wait,notify,notifyAll注意点
public void text(Object o){
synchronized(o){
try {o.wait();} catch (Exception e) {}
try {o.notify();} catch (Exception e) {}
try {o.notifyAll();} catch (Exception e) {}
}
}
- wait,notify,notifyAll必须在synchronized的代码块中 2. o.notify() 只能唤醒被Obect类型修饰的o对象的wait,假如换做o2的object则不不可以,o2.notify()只能o2的wait
何时使用
- 需要保证线程安全就只使用关键字synchronized即可
- 在多个线程中需要交互,例如线程1,执行到某段逻辑之后,需要先执行线程2的代码,类似这样的情况下就可以使用wait,notify,notifyAll
wait跟sleep的区别
出处不同:
- wait来自于Object
- sleep来自于Thread
锁的持有不同:
- wait执行后会将当前线程挂起,并且会释放锁,而其他的线程可以执行同步代码块
- sleep执行后会将当前线程设定为阻塞状态且不会释放所,其他线程无法执行同步代码块
notify跟notifyAll的区别
- nofity随机唤醒一个被同一对象修饰限定的等待的线程
- notifyAll唤醒所有被同一对象修饰限定的等待的线程
二:join()方法:
三:ThreadLocal的使用
ThreadLocal介绍:
在java类中,创建的被private修饰的成员变量,被当前类所使用,且成员变量的生命周期随从于当前类,随着类的创建而存在,随着类的消失而消失;
而ThreadLocal理解上和java类中的成员变量是类似的,ThreadLocal归于线程,随着线程的创建调用ThreadLocal而存在,随着线程的消失而消失
ThreadLocal使用:
ThreadLocal<Object> threadLocal = new ThreadLocal<Object>() {
@Override//未set()get()方法会返回该初始值
protected Object initialValue() {return null;}
};
threadLocal.set("1");//存储操作
threadLocal.get();//获取操作
threadLocal.remove();//移除操作
ThreadLocal注意事项:
需要注意的一点是ThreadLocal不像list集合等可以进行多组数据的存储,ThreadLocal只能保存任意类型的一组数据
四:Condition的使用
Condition介绍:
从使用上来说,Condition就是Lock和synchronize功能的结合体,其包含了Lock的功能,并且还能给进行await和signal(也就是在synchronize中的notify功能)
Condition具体使用如下:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition boyCondition = lock.newCondition();
Condition girlCondition = lock.newCondition();
Thread boy = new Thread(() -> {
//保障girl线程先跑起来
try {Thread.sleep(100);} catch (InterruptedException e) {}
lock.lock();
System.out.println("1.我已经在咖啡厅等你一个小时了");
girlCondition.signal();
try {boyCondition.await();} catch (InterruptedException e) {}
System.out.println("3.不等了我要去打王者了");
girlCondition.signal();
try {boyCondition.await();} catch (InterruptedException e) {}
System.out.println("5.你化完给我拍个照片算了,不当面看了");
System.out.println("6.自此,这个男孩失去了女票,男孩痛苦的自问:王者这打到了一百星又能怎么样呢???从此男孩一蹶不振,整日沉迷游戏,勉为其难的接受众多女粉丝的追捧......");
lock.unlock();
});
boy.start();
Thread girl = new Thread(() -> {
lock.lock();
// System.out.println("半小时后女孩打开手机");
try {girlCondition.await();} catch (InterruptedException e) {}
System.out.println("2.我在化妆请再等一小时");
boyCondition.signal();
try {girlCondition.await();} catch (InterruptedException e) {}
System.out.println("4.那我这一个小时白画了,不行!");
boyCondition.signal();
lock.unlock();
});
girl.start();
}
//输出结果:
1.我已经在咖啡厅等你一个小时了
2.我在化妆请再等一小时
3.不等了我要去打王者了
4.那我这一个小时白画了,不行!
5.你化完给我拍个照片算了,不当面看了
6.自此,这个男孩失去了女票,男孩痛苦的自问:王者这打到了一百星又能怎么样呢???从此男孩一蹶不振,整日沉迷游戏,勉为其难的接受众多女粉丝的追捧......
五(工具类):CountDownLatch
- await():将当前现在置为等待的状态
- countDown():计数器减一;当减到0的是时候就会去调用执行被await的线程
- 举例 : 王者荣耀里集结队友打大龙
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(()->{
try {
System.out.println("currentThreadName: "+Thread.currentThread().getName()+" "+"打野:对面已团灭快过来打大龙~");
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("currentThreadName: "+Thread.currentThread().getName()+" "+"大龙被击杀!!!");
System.out.println("currentThreadName: "+Thread.currentThread().getName()+" "+"说时迟那时快,对面复活的鲁班一个飞过来的导弹把大龙抢走了");
}).start();
String[] Arr = new String[]{"射手","法师","辅助","上单"};
for (int i = 0; i < 4; i++) {
int finalI = i;
new Thread(()->{
try {
System.out.println("currentThreadName: "+Thread.currentThread().getName()+" "+Arr[finalI]+" :我来了");
Thread.sleep(finalI * 300l);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
}
//输出结果
currentThreadName: Thread-0 打野:对面已团灭快过来打大龙~
currentThreadName: Thread-1 射手 :我来了
currentThreadName: Thread-2 法师 :我来了
currentThreadName: Thread-3 辅助 :我来了
currentThreadName: Thread-4 上单 :我来了
currentThreadName: Thread-0 大龙被击杀!!!
currentThreadName: Thread-0 说时迟那时快,对面复活的鲁班一个飞过来的导弹把大龙抢走了
上面的例子可以看的到new CountDownLatch(4)限定了需要countDown()的次数,在循环四次调用了四次countDown()后,才继续执行countDownLatch.await();后面代码,才击杀大龙
(工具类)CyclicBarrier–栅栏
CyclicBarrier介绍:
- 用于一组线程,两个线程到达指定状态后再一起执行,假如只有一条线程那该线程就会一直处于等待状态
- 更形象的解释就是 : 如同晚上十二点约女朋友去看电影,先一起到酒店门口集合,等都到了酒店门口,再一起去看电影;假如是单数,是一个单身狗,就算在酒店门口站到天荒地老也等不到女朋友晚上十二点一起看电影的
跟countDownLatch的区别:
- CountDownLatch : 用于当前线程执行到await后挂起,然后等待其他线程执行完任务之后,再继续执行且不可重复使用
- CyclicBarrier : 用于一组线程,两个线程到达指定状态后再一起执行,假如只有一条线程那该线程就会一直处于等待状态 可重用的
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
for (int i = 0; i < 6; i++) {
int finalI = i;
new Thread(() -> {
try {
Thread.sleep(finalI * 500L);
System.out.println(Thread.currentThread().getName() + "准备就绪");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("开始赛跑");
}).start();
}
}
(工具类)Semaphore信号量的使用
Semaphore介绍:
控制并发数量
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 20; i++) {
new Thread(()->{
try {
//每次只会创建两个线程执行,当十秒后再创建两条,依次循环
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "666");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
(工具类)Exchanger
Exchanger介绍:
- 两个线程之间交换数据
- 它和CyclicBarrier类似,用于一组线程,两个线程到达指定状态后再一起执行,假如只有一条线程那该线程就会一直处于等待状态
public static void main(String[] args) {
Exchanger<String> stringExchanger = new Exchanger<>();
String roleADC = "ADC";
String rolesubsequent = "辅助";
String dog = "我挂机...";
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " :一号玩家最初想玩:" + roleADC);
try {
String exchange = stringExchanger.exchange(roleADC);
System.out.println(Thread.currentThread().getName() + " :交換后一号玩家玩:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " :二号玩家最初想玩:" + rolesubsequent);
try {
String exchange = stringExchanger.exchange(rolesubsequent);
System.out.println(Thread.currentThread().getName() + " :交換后二号玩家玩:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " :挂机玩家:" + dog);
try {
String exchange = stringExchanger.exchange(dog);
System.out.println(Thread.currentThread().getName() + " :挂机后:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程3").start();
}
// 输出结果:
线程1 :一号玩家最初想玩:我玩ADC
线程2 :二号玩家最初想玩:我玩辅助
线程2 :交換后二号玩家玩我玩ADC
线程1 :交換后一号玩家玩:我玩辅助
线程3 :挂机玩家:我挂机...
- 由此可见Exchanger必须成对使用,挂机玩家就再也没有出现过
5.单例与线程安全
一:单例与线程安全
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
private HungerSingleton() {
}
}
注意:
- 饿汉式单例,在类加载的时候,就已经进行实例化,假如该实例对象内存占用高,实例化后长期并未使用这就会导致浪费内存.
二:懒汉式单例
/**
* 懒汉式单例
* 在需要的时候再实例化
*/
public class LazySingleton {
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//判断实例是否为空,为空则实例化
if (null == lazySingleton) {
//模拟实例化时耗时的操作
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LazySingleton.class) {
if (null == lazySingleton) {
//正常的加载顺序是: 1. 分配对象的内存空间 2. 加载对象 3. 将对象指向分配的内存空间
// 重排序的情况下将是: 1,3,2 当A线程执行完第一步操作后,因为指令重排序,先执行第三步骤,则这个时候对象不为null,这时,B线程线程进行调用if (null == lazySingleton)时候就会得到不为null
//所以lazySingleton必须要加volatile 关键字修饰
lazySingleton = new LazySingleton();
}
}
}
//否则直接返回
return lazySingleton;
}
}
三:枚举方式单例
public enum EnumSingletion {
INSTANCE;
private Object singletion;
//JVM保证整个方法只被调用一次
EnumSingletion(){
singletion = new Object();
}
public Object getSingletion() {
return singletion;
}
}