线程安全
Posted jrliu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全相关的知识,希望对你有一定的参考价值。
public class TestSync implements Runnable int b = 100; synchronized void m1() throws InterruptedException b = 1000; Thread.sleep(500); //6 System.out.println("b=" + b); synchronized void m2() throws InterruptedException Thread.sleep(250); //5 b = 2000; public static void main(String[] args) throws InterruptedException TestSync tt = new TestSync(); Thread t = new Thread(tt); //1 t.start(); //2 tt.m2(); //3 System.out.println("main thread b=" + tt.b); //4 @Override public void run() try m1(); catch (InterruptedException e) e.printStackTrace(); 输出可能是: b = 1000 main thread b= 2000 or main thread b= 2000 b = 1000 or main thread b=2000 b=1000
-
synchronized
-
线程的几个状态:new,runnable(thread.start()),running,blocking(Thread.Sleep())
-
在什么样的环境:多个线程的环境下。
-
在什么样的操作:多个线程调度和交替执行。
-
发生什么样的情况: 可以获得正确结果。
-
谁 : 线程安全是用来描述对象是否是线程安全。
2.2.1不可变
2.2.2绝对线程安全
public Object getLast(Vector list) return list.get(list.size() - 1); public void deleteLast(Vector list) list.remove(list.size() - 1);
2.2.3相对安全
public synchronized Object getLast(Vector list) return list.get(list.size() - 1); public synchronized void deleteLast(Vector list) list.remove(list.size() - 1);
2.2.4线程兼容
2.2.5线程对立
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程(或是一些,使用信号量的时候)线程使用。
2.3、线程安全实现方法 2.3.1 互斥同步
-
如果我们显示的使用lock我们得手动的进行解锁unlock()调用,但是很多人在实际开发过程其实有可能出现忘记,所以推荐使用synchronized ,在易于编程方面Lock败。
-
synchronized 在jdk1.6之后对其进行了优化会从偏向锁,轻量级锁,自旋适应锁,最后才到重量级锁。而Lock一来就是重量锁。在未来的jdk版本中,重点优化的也是synchronized。在性能方便Lock也败。
2.3.2 非阻塞同步
2.3.3 无同步
-
可重入代码:可重入代码也叫纯代码,可以随时中断,恢复控制权之后程序依然不会出任何错误,可重入代码的结果一般来说是可预测的:
public int sum() return 1+2;
-
线程本地存储:而这个一般来说是我们用得比较多的手段,我们可以通过保证类是无状态的,所有的变量都存在于我们的方法之中,或者通过ThreadLocal来进行保存。
-
在使用某些对象作为单例的时候,需要确定这个对象是否是线程安全的: 比如我们使用SimpleDateFormate的时候,很多初学者都不注意将其作为单例一个工具类来使用,导致了我们的业务异常。
-
如果发现其不是单例,需要进行替换,比如HashMap用ConcurrentHashMap,queue用ArrayBlockingQueue进行替换。
-
注意死锁,如果使用锁一定记得释放锁,同时使用锁的顺序一定要注意,这里不仅仅说的是单机的锁,也要说分布式锁,一定要注意:一个线程先锁A后锁B,另一个线程先锁B后锁A这个情况。所以一般来说分布式锁会加上超时时间,避免由于网络问题释放锁失败,而导致死锁。
-
锁的粒度:同样的不仅仅是说单机的锁,也包括了分布式锁,不要图方便直接从入口方法,不加分析的就开始加锁,这样会严重影响性能。同样的也不能过于细粒度,单机的锁会增加上下文的切换,分布式锁会增加网络调用,都会导致我们性能的下降。
-
适当引入乐观锁:比如我们有个需求是给用户扣款,为了防止多扣,这个时候会用悲观锁进行锁,但是效率比较低,因为用户扣款其实同时扣的情况是比较少的,我们就可以使用乐观锁,在用户的账户表里面添加version字段,首先查询version,然后更新的时候看看当前version和数据库的version是否一致,一致就更新不一致就证明已经扣过了。
-
如果想要在多线程环境下使用非线程安全对象,数据可以放在ThreadLocal,或者只在方法里面进行创建,我们的ArrayList虽然不是线程安全的,但是一般我们使用的时候其实都是在方法里面进行List list = new ArrayList()使用,用无同步的方式也保证了线程安全。
多线程(四):线程安全
多线程(四):线程安全
观察线程安全和线程不安全
观察线程安全
单线程是线程安全的,示例代码如下:
public class ThreadDemo25 {
static class Counter {
//定义私有变量
private int num = 0;
//定义任务执行次数
private final int maxSize = 100000;
//num++
public void incrment() {
for (int i = 0; i < maxSize; i++) {
num++;
}
}
//num--
public void decrment() {
for (int i = 0; i < maxSize; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
counter.incrment();
counter.decrment();
System.out.println("最终执行结果:" + counter.getNum());
}
}
该代码的执行结果如下:
观察线程不安全
多线程可能是线程不安全的,示例代码如下:
public class ThreadDemo26 {
static class Counter {
//定义私有变量
private int num = 0;
//定义任务执行次数
private final int maxSize = 100000;
//num++
public void incrment() {
for (int i = 0; i < maxSize; i++) {
num++;
}
}
//num--
public void decrment() {
for (int i = 0; i < maxSize; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
counter.incrment();
});
t1.start();
Thread t2 = new Thread(() -> {
counter.decrment();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终执行结果:" + counter.getNum());
}
}
若是线程安全的话,代码执行结果,应该是与前者一致的,但实际执行结果如下:
线程安全概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的;反之,这个程序就是线程不安全的。
导致线程不安全的原因
比如刚才我们看到的 num++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
不保证原子性会给多线程带来的问题:
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
原子性:
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
一条 java 语句不一定是原子的,也不一定只是一条指令。
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
示例代码如下:
public class ThreadDemo27 {
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置flag = true");
flag = true;
}
});
t2.start();
}
}
代码执行结果如下:
我们发现,该代码的预期执行结果,应该是当运行flag = true;
代码时,t1线程的while循环条件不满足,跳出循环并执行System.out.println("终止执行");
,但是,由于内存不可见,造成t1线程中while死循环现象。
线程非安全的原因:
- CPU抢占 执行(万恶之源)
- 非原子性,如图示
- (内存)不可见
- 编译器优化(代码优化)
- 多个线程修改了同一个变量
编译器优化:在单线程下执行,没有问题,可以提高程序执行的效率;但是在多线程下就会出现混乱,从而导致线程不安全。
演示:多线程操作并未修改相同变量,那么线程也是安全的,代码如下,
public class ThreadDemo28 {
static class Counter {
//定义任务执行次数
private final int maxSize = 100000;
//num++
public int incrment() {
int num1 = 0;
for (int i = 0; i < maxSize; i++) {
num1++;
}
return num1;
}
//num--
public int decrment() {
int num2 = 0;
for (int i = 0; i < maxSize; i++) {
num2--;
}
return num2;
}
}
private static int num1 = 0;
private static int num2 = 0;
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
num1 = counter.incrment();
});
t1.start();
Thread t2 = new Thread(() -> {
num2 = counter.decrment();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终执行结果:" + (num1 + num2));
}
}
该代码执行结果如下:
可见,该程序的多线程是安全的。
以上是关于线程安全的主要内容,如果未能解决你的问题,请参考以下文章