文章目录
前言
本篇文章主要讲述线程不安全的原因以及解决方案。
一、线程不安全的原因
1.操作系统调度的随机性,抢占式执行;
2.多个线程修改同一个变量;’
3.修改操作不是原子的;
4.内存可见性问题(JVM的代码优化引入的bug);
5.指令重排序。
线程不安全远远不止这种原因,而以上总结的这些情况一定要具体代码具体分析。
二、线程不安全的解决方案
1.加锁synchronized
将并发变成了串行。
(1)修饰普通方法,锁对象相当于this。
代码如下(示例):
class Counter {
public int count = 0;
//加锁
public synchronized void increase() {
count++;
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5_0000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
(2)修饰代码块,锁对象在()指定。
代码如下(示例):
class Counter {
public int count = 0;
public void increase2() {
//加锁
synchronized (this) {
count++;
}
}
}
public class Demo2 {
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 1_0000; i++) {
counter.increase2();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 1_0000; i++) {
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
(3)修饰静态方法,锁对象相当于类对象(不是锁整个类)
代码如下(示例):
class Counter {
public int count = 0;
public static Object locker = new Object();
public void increase2() {
//加锁
synchronized (locker) {
count++;
}
}
}
public class Demo3 {
public static Counter counter = new Counter();
public static Counter counter2 = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 1_0000; i++) {
counter.increase2();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 1_0000; i++) {
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
(4)可重入锁
1.让锁里持有线程对象,记录是谁加了锁。
2.维护一个计数器,用来衡量啥时候是真加锁,啥时候是真解锁,啥时候是直接放行。
2.volatile:保证内存可见性
volatile保证内存可见性但不保证原子操作;保证每次读内存都是真的从主内从中重新读取。
volatile的作用:
(1)保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
(2)保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
代码如下(示例):
public class Demo3 {
static class Counter {
//volatile保证每次都真的直接从主内存中重新读取
public volatile int count = 0;
public void increase() {
count++;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
//如果count不使用volatile,线程在执行的时候就编译器默认优化只读一次内存后默认count=0
while (counter.count == 0) {
}
System.out.println("线程t1结束");
});
t1.start();
Thread t2 = new Thread(()->{
System.out.println("请输入count=");
Scanner sc = new Scanner(System.in);
counter.count = sc.nextInt();
});
t2.start();
}
}
3.wait和notify:控制线程调度顺序
(1)wait()结束等待的条件:释放当前锁;进行等待通知;满足一定条件的时候(别人调用notify),被唤醒后尝试重新获取锁。
(2)notify()唤醒等待的进程:
代码如下(示例):
public class Demo4 {
public static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (object) {
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
}
});
t1.start();
Thread.sleep(500);
Thread t2 = new Thread(() -> {
while (true) {
synchronized (object) {
System.out.println("notify之前");
object.notify();
System.out.println("notify之后");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
}
wait与sleep的区别(面试题)
1、共同点:都是使线程暂停一段时间的方法。
2、不同的:
(1)wait是Object类中的一个方法,sleep是Thread类中的一个方法;
(2)wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;
(3)wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作。
三、单例模式
1.饿汉模式
代码如下(示例):
//单例类 饿汉模式
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
//将构造方法改为私有的,防止出现多个实例
private Singleton() {
}
}
2.懒汉模式
代码如下(示例):
//懒汉模式--线程不安全
class SingletonLazy {
private static SingletonLazy instance = null;
private static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
3.修改懒汉模式
在多线程下,上方懒汉模式是不安全的,通过加锁可以将其变成线程安全的。
代码如下(示例):
//懒汉模式--线程不安全--如何修改使它变成线程安全的--通过加锁
class SingletonLazy {
//加上volatile禁止指令重排序
private volatile static SingletonLazy instance = null;
private static synchronized SingletonLazy getInstance() {
if(instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}