1.线程安全
1.1.观察线程不安全
实例:
java
package thread;
public class text18 {
//定义一个成员变量count,初始值为0
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
以上代码中,我们理想的count结果是为10000,但是我们看下真实情况。
结果1:

结果2:

可以看到,两个运行结果不仅不是10000,而且两个运行结果还不相同,这便是线程不安全。
1.2.线程安全的概念
一段代码,在多线程的运行下,如果它运行出来的结果与单线程运行出来的结果相同时,那么我们就可以说这个线程是安全的。
1.3.线程不安全的原因
线程随机性
- 多线程的调度是随机性的,这个随机性就是线程不安全的主要因素。
- 线程调度在一个环境下,它执行的顺序是不相同的。
修改公共数据
例如1.1中,两个线程修改一个的count变量。
这时候的count变量就是可以被多个线程修改的"公共数据"。
原子性
什么是原子性
我们可以把一段代码想象成一个的厕所,这时候张三去上厕所没把门锁住,正在进行到一半,老六进来了在张三背后说我也要上厕所,这时候张三就给老六吓得强行打断施法,并对老六破口大骂。
同时我们把1.1的例子带入到张三老六身上,我们就可以说1.1例子中的代码没有原子性;
但如果张三把门锁了,也就是t2的线程等t1的线程运行完之后再运行,我们就可以说这段代码有原子性。
我们会把以上现象称为"同步互斥",表示这个操作是互相排斥的。
一条JAVA语句不一定是一条指令
在1.1的例子中,count就有三条指令
- load 把内存中的值加载到寄存器中
- add 在寄存器中进行+1的操作
- save 将寄存器中操作过后的值放回内存中
不保证原子性会带来什么问题
例如1.1中,最后的cont就达不到我们理想中的数值
其原因就是t2的线程在t1还没完整的运行load,add,save这三个指令后就打断t1,使它得出错误的结果。
可见性
可见性是指,一个线程中的变变量能够被其他线程所看到。
1.4.解决之前的线程不安全问题
加个synchroized关键字
java
package thread;
public class text19 {
//创建count成员变量
public static int count=0;
//创建静态的Object类的lock(锁)
public static final Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
//使用synchronized来锁定lock对象
synchronized(lock){
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
//使用synchronized来锁定lock对象
synchronized(lock){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
2.synchroized 关键字
2.1.synchroized的特征

互斥
synchroized具有互斥性,当一个线程使用synchroized锁住一个对象,同时另一个线程也用synchroized锁住同一个对象,这时候另一个线程就会堵塞。
可以把一个线程想象成一个厕所,当李四进入厕所时,他就会使用sychroized将门锁住,这时候王五想上厕所,那么因为门给锁住了,王五只能在门外,等李四上完之后,王五就获得了李四的锁,然后就进去上厕所再把门锁上,就不会像上文中张三老六发生尴尬的现象。
同理,例如1.1例子中,如果两个线程都用sychroized锁住了同一个对象,那么只能等t1线程解锁运行完之后,然后t2获得了锁再运行。
注意:
在上一个线程运行完解锁之后,如果有多个线程,那么系统会随机唤醒某个线程,而不是按照写代码的顺序唤醒,例如如果有t1,t2,t3线程,在t1线程解锁之后,等待的t2和t3线程中的其中一个就会获得锁,可能是t2也可能是t3。
可以理解为李四上完厕所出来手上拿着锁,在门外的张三和老六都想上厕所,然后他们就开始了争夺锁,最后可能是老六得到了锁也可能是张三得到了锁,但不管是谁得到了锁,到了最后他们都能成功的上完厕所~~
可重入
synchronized对于同一个线程来说是可重入的,就是可以写多条synchronized语句,不会造成死锁的情况。
死锁:
死锁就是写入一条syncronized语句时,还没解锁就写入另一条synchronized,这时候如果要解锁里面的syncronized就需要先解锁外面一层的synchronized,然而理想是丰满的现实是残酷的,有两个条锁的情况下我们并不能把这两条锁都解除掉,当第一个锁解除后这条线程也就停止了运行,第二个锁就解不了了,这就造成了死锁现象。

但是JAVA的synchronized是可重入锁,这就为我们解决了死锁的问题。
在可重入中,包含了"线程持有者"和"计时器"两个信息
"线程持有者"记录了当前的前程,当发现有线程占用锁时,但那个占用锁的人是自己本身,那么仍可以获取到锁,并让"计时器"加1;
解锁的时候,当计时器为0时,这条线程才为解锁状态。

2.2.synchronized使用示例
基础用法:
java
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
//使用synchronized锁住lock对象
synchronized(lock){
count++;
}
}
});
变为方法使用:
java
//1.创建一个方法,在方法内调用synchronized
public static void add(){
synchronized(lock){
count++;
}
}
//2.创建一个synchronized 类的方法
synchronized public static void add(){
count++;
}
2.3.JAVA标准库中的线程安全类
JAVA库中的很多都是线程不安全的,他们都涉及数据的共享与修改,以下是一些线程安全的
- Vector
- HashTable
- ConcurrenHashMap
- StringBuffer
3.volatile关键字
3.1.volatile能保证内存可见性
实例:
java
package thread;
import java.util.Scanner;
//写两个线程,通过t2线程来改变t1线程
public class Demo27 {
//定义f成员变量
static int f=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
//f=0时,while循环一直运行0
while(f==0){
//dosomthing
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner in = new Scanner(System.in);
//改变f的值,让t1的while循环结束
System.out.println("请输入一个数");
f=in.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
我们理想的结果应该是输入任意一个非0值,然后打印"t2线程结束""t1线程结束",可结果却是

只打印了 "t2线程结束",t1的线程还在运行,那么我们就可以通过该结果得出我们写的代码有一定的问题的
这就涉及到了"可见性",在t2线程中修改的f变量,t1中没有发现
这其实也是编译器的优化,所谓的编译器优化是指如果大量读取一个相同的数据,那么接下来都会默认是该数据
首先每个JAVA进程都有一个"主内存",而每个JAVA线程都有自己的"工作内存",每个JAVA线程先从内存中读取数据到自己的工作内存,然后进行工作
所以上述代码中,t1线程将f=0读取到自己的工作内存(此时触发了编译器优化),然而t2修改的是主内存中的数据,t1的工作内存中的数据仍然是f=0,两者不会有影响。

这时候我们就需要使用volatile
java
public class Demo27 {
//在f成员变脸添加volatile
static volatile int f=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(f==0){
}
System.out.println("t1线程结束");
});
Thread t2=new Thread(()->{
Scanner in = new Scanner(System.in);
System.out.println("请输入一个数");
f=in.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
结果:

- 代码运行时volatile从主内存读取最新的值返回给java线程的工作内存中
- volatile可以保证"内存可见性"
- 我们可以理解为使用volatile相当于告诉编译器让他停止优化功能
- 虽然运行速度慢了,但是数据更准确了
3.2.volatile不能保证原子性
- volatile和synchroized有本质区别
- volatile:保证"内存可见性"
- synchroized:保证"原子性"
4.wait和notify
由于线程之间是抢占式执行,所以线程的执行顺序是不稳定的,但我们需要使线程之间有序进行,所以就需要wait()和notify()两个方法。
4.1.wait()方法
功能:
- 让当前进程等待运行
- 释放当前的锁
- 当满足一定条件时被唤醒,并尝试重新获得锁
结束条件:
- 使用notify()唤醒
- wait自带的等待时间超时(wait中自带timeoutMillis,设置等待的时间)

实例:
java
package thread;
public class test20 {
public static void main(String[] args) throws InterruptedException {
//创建锁
Object lock=new Object();
//创建线程
Thread t1=new Thread(()->{
System.out.println("wait之前");
//让线程等待
//因为wait方法必须在同步代码块中使用
//所以要先获取锁
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("wait之后");
});
//启动线程
t1.start();
}
}
结果:

以上代码中运行后只打印了"wait之前",我们再来看一下JAVA的管理控制台

可以看到我们创建的该线程的状态为等待状态
那么就说明了wait方法是将当前线程变为等待状态
4.2.notify()方法
功能:
- 唤醒一个正在等待的线程
- 只能唤醒一个等待的线程,如果由多个等待线程,notify则随机唤醒一个
- 在notify之后,不会马上释放锁,而是等待notify当前的同步代码块运行完才释放锁
实例1:
创建一个等待线程和一个唤醒线程
java
package thread;
public class test20 {
public static void main(String[] args) throws InterruptedException {
//创建锁
Object lock=new Object();
//创建线程--等待线程
Thread t1=new Thread(()->{
System.out.println("wait之前");
//让线程等待
//因为wait方法必须在同步代码块中使用
//所以要先获取锁
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("wait之后");
});
//创建线程--唤醒线程
Thread t2=new Thread(()->{
System.out.println("notify之前");
//唤醒线程
//因为notify方法必须在同步代码块中使用
//所以要先获取锁
synchronized(lock){
lock.notify();
}
System.out.println("notify之后");
});
//启动线程
t1.start();
t2.start();
}
}
结果:

实例2:
创建两个等待线程和一个唤醒线程
java
package thread;
public class text22 {
public static void main(String[] args) throws InterruptedException {
//创建锁
Object lock=new Object();
//创建线程--等待线程
Thread t1=new Thread(()->{
System.out.println("wait1之前");
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("wait1之后");
});
Thread t2=new Thread(()->{
System.out.println("wait2之前");
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("wait2之后");
});
//创建线程--唤醒线程
Thread t=new Thread(()->{
System.out.println("notify之前");
//唤醒线程
//因为notify方法必须在同步代码块中使用
//所以要先获取锁
synchronized(lock){
lock.notify();
}
System.out.println("notify之后");
});
//启动线程
t1.start();
t2.start();
t.start();
}
}
结果:

运行后只唤醒了t1线程
所以notify只能唤醒一个线程
4.3.notifyAll ()方法
功能:
- 唤醒所有线程
实例:
创建三个等待线程和一个唤醒线程
java
package thread;
public class text21 {
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t1=new Thread(()->{
System.out.println("wait1之前");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait1之后");
});
Thread t2=new Thread(()->{
System.out.println("wait2之前");
synchronized(lock){
try{
lock.wait();
}catch (InterruptedException e){
throw new RuntimeException(e);
}
}
System.out.println("wait2之前");
});
Thread t3=new Thread(()->{
System.out.println("wait3之前");
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("wait3之前");
});
Thread t=new Thread(()->{
System.out.println("notifyAll之前");
synchronized(lock){
lock.notifyAll();
}
System.out.println("notifyAll之前");
});
t1.start();
t2.start();
t3.start();
t.start();
}
}
结果:

可以看到所有等待线程都被唤醒,但是唤醒的顺序不同,因为是同时唤醒,notify一时间不知道先该唤醒谁,所以就导致了线程竞争。
4.4.notify 和 notifyAll的对比
可以想象notify/notifyAll和wait为老师和学生
notify老师检查背诵的时候,喜欢一个一个按号数叫wait学生起来背诵
notifyAll老师检查背诵的时候则是:"你们谁要来背诵,先背完的就可以走了",那么好几个wait学生就争抢着取背诵~~
- notify为唤醒单个线程,有序
- notifyAll为同时唤醒多个线程,无序
4.5.wait 和 sleep 的对比
相同点:
- wait和sleep都可以使线程等待一段时间
- 都有超时时间。
不同点:
- wait是Object下的方法,sleep是Thread下的静态方法
- wait它的设计是为了提前唤醒,sleep它的设计是为了到点唤醒
- wait提前唤醒时不会报错,sleep提前唤醒时会报错
- wait需要上锁,sleep不需要上锁