目录
3.解决线程不安全之一Synchronized关键字(监视器锁)
线程安全一的博客在这里喔~
线程安全【一】
1.线程不安全是什么?
如果多线程环境下代码运行的结果与单线程环境下不一致时就会出现线程不安全的问题
2.线程不安全的成因
3.解决线程不安全之一Synchronized关键字(监视器锁)
加锁之后把并行变为了串行,所以实现了内存可见,因为串行一定是内存可见的
1.Synchronized使用方法
2.锁对象是什么?
3.锁对象的练习
1.定义两个线程分别自增五万次
public class Demo18 {
public static void main(String[] args) throws InterruptedException {
Count01 count = new Count01();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = "+count.count);
}
}
class Count01{
public int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
是同一个Count对象,所以线程安全,结果正确
4.Synchronized的特性
1.互斥性
同一时间只有一个线程可以获取到锁,其他的线程都要阻塞等待
2.刷新内存
线程修改完变量的值后,会把新值写回主内存,但这是用并行转为串行的方式做到的
3.可重入
Synchronized是可以嵌套使用的,当前线程每重新获取一次锁,计数器就+1,每释放一次锁,计数器就-1,直至计数器为0,当前线程才会真正的释放锁
5.总结
synchronized可以解决原子性,内存可见性,但是不能解决有序性的问题
并且synchronized并没有真正的通过线程之间的通信解决内存可见性,也没有解决有序性的问题
4.解决线程不安全之一Volatile关键字
1.首先我们来分析一下CPU对代码的执行
我们创建两个线程,一个线程不停的循环判断标识位,用第二个线程来修改标识位
代码展示:
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("t1开始============");
while (count == 0) {
//to do
}
System.out.println("t1结束============");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("t2开始============");
System.out.println("请输入一个数字:");
Scanner sc = new Scanner(System.in);
count = sc.nextInt();
System.out.println("t2结束============");
});
TimeUnit.SECONDS.sleep(1);
t2.start();
}
结果我们发现,在第二个线程修改了标识位之后,第一个线程并没有停止运行
2.将变量用volatile修饰
public static volatile int count = 0;
此时我们发现程序可以正常退出了,证明t1及时接收到了其他线程对变量修改后的值,解决了内存可见性的问题
不建议使用:
3.缓存一致性协议
可以理解为一种通知机制,正是这个协议的存在解决了内存不可见的问题
4.内存屏障
对加了volatile的变量,加了以下内存屏障
当某个线程对变量发生写操作之后,就会通过缓存一致性协议来通知其他的线程缓存值失效
所以volatile可以解决内存可见性
5.内存有序性,原子性
内存有序性是指在保证程序正确的前提下,编译器,CPU对指令的优化过程
volatile可以解决有序性问题
用volatile修饰的变量,就是要告诉编译器,不需要对这个变量所涉及到的操作做任何优化,从而实现有序性
volatile只用来修饰变量
volatile不具备原子性
通过synchronized和volatile关键字的搭配使用解决了原子性,内存可见性,有序性的问题
5.synchronized和volatile关键字的区别
6.wait和notify关键字
wait()方法和nitify()方法都是Object类中的方法
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notify方法只是唤醒(CPU随即调度,并无先来后到)某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程(所有线程共同参与锁的竞争).
在使用过程中,wait和notify必须是同一个锁对象,且wait和notify被调用后,当前线程都会释放锁资源
join()是Thread类中定义的方法
7.wait()方法和sleep()方法的区别
线程1调用sleep()方法后,进入休眠,线程进入阻塞等待的状态,此时并不会释放锁,也不会去执行其他线程,等到等待时间结束后,线程1会继续执行后面的代码块
线程一在调用wait(10000)方法之后,线程一会进入等待状态,在等待的这十秒钟时间内,CPU可以去调用别的线程,等到等待时间结束后且CPU正在调度的线程结束释放锁之后,CPU才会回来继续执行线程一的代码块
8.Java中线程安全的类
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer