前言
记录学习过程
前面实现了线程与多线程的创建及一些操作方法;
可以看出许多在单线程有用的方法到了多线程就会出各种错误;
为了解决错误,多线程并发就有很多知识
并发的关键字:Volatile、synchronized
这个大佬的解释很深入
目录
线程安全
许多单线程的程序到了多线程就会线程不安全
例如最基础的 读取 - 修改 -写入 问题,对于多线程操作一个共享变量,执行两次++,可能就加了一个1
那么线程安全是什么呢?
线程安全:当一个类,不断被多个线程调用,仍能表现出正确的行为时,那它就是线程安全的
为了实现线程安全,要从两个方面去考虑:执行控制和内存可见
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存
synchronized关键字解决的是执行控制的问题,synchronized是锁机制,当有线程使用方法时将当前对象、类锁住,使其他线程不能调用该方法,就杜绝了多线程同时调用导致程序实现错误的情况
volatile关键字解决的是内存可见性的问题,由于线程操作的对象是拷贝的副本,不是原对象,造成明明程序改动了变量,实际情况却没有改动的问题,volatile修改变量并同步到内存
Java内存模型(JMM)
Java内存模型(Java Memory Model )定义了 Java 虚拟机 ( JVM ) 在计算机内存 ( RAM ) 中的工作方式
这个解释很详细
多线程并发编程的两个问题:
- 线程间的通信
即如果有共享数据,就可以通过共享数据通信
没有共享就只能通过明确的发送信息方法通信 - 线程同步
控制不同进程间操作发生的相对顺序的机制
如锁机制互斥执行,消息传递就要顺序执行
JMM定义了线程的私有本地内存(抽象概念,并不真实存在)与内存的共享变量的关系
package com.company.Thread;
public class ThreadOfPool {
private static int count=1;
static class CreatThread1 extends Thread {
public void run(){
count++;
System.out.println("线程1:"+count);
}
}
static class CreatThread2 extends Thread {
public void run() {
count++;
System.out.println("线程2:"+count);
}
}
public int getCount(){
return count;
}
public static void main(String[] args) throws InterruptedException {
CreatThread1 creatThread1=new CreatThread1();
CreatThread2 creatThread2=new CreatThread2();
creatThread1.start();
creatThread2.start();
}
}
关于 读取 - 修改 -写入 问题,线程A creatThread1 读取了共享变量放在本地内存,然后count+1,在这时线程B creatThread2也读取了共享变量,这时count还是1,然后++,线程a是count++=2,线程b也是count++=2,这就不符合程序的原意
通过锁定共享变量,使得变量count具有可见性就可以解决这个问题
Volatile
这个案例挺符合的:
package com.company.Thread.Game;
public class GoalNotifier implements Runnable {
public boolean goal = false;
public boolean isGoal() {
return goal;
}
public void setGoal(boolean goal) {
this.goal = goal;
}
@Override
public void run() {
while (true) {
if (isGoal()) {
System.out.println("Goal !!!!!!");
setGoal(false);
}
}
}
}
当goal = true 时会执行run中的语句
测试:
package com.company.Thread.Game;
public class Game {
public static void main(String[] args) throws InterruptedException {
// Game begun! Init goalNotifier thread
GoalNotifier goalNotifier = new GoalNotifier();
Thread goalNotifierThread = new Thread(goalNotifier);
goalNotifierThread.start();
// After 3s
goalNotifierThread.sleep(3000);
// Goal !!!
goalNotifier.setGoal(true);
}
}
并不会输出,因为goalNotifier.setGoal(true)方法操作的是本地内存中的goal变量,所以方法没反应,因为他操作的是主存中的goal变量
使用Volatile修饰goal变量:
public volatile boolean goal = false;
就运行了run方法
Volatile是一个标志,通知JVM在编译时应该从主存读取变量
在编译过程中会多一行机器语言:lock add dword ptr
翻译:lock的作用是使得本CPU的Cache写入内存,同时使其他CPU的Cache无效
volatile不能保证原子性。但能保证可见性和有序性
而synchronized能保证原子性
Volatile修饰的变量保证了3点:
- 完成写入后,所以访问该变量的进程都会得到最新值(可见性)
- 在你写入前,会保证所有之前发生的事已经发生(这是Volatile修饰的前提)
- volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方
volatile大多用于标志位上(判断操作),使用Volatile的前提:
- 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
- 该变量不会纳入到不变性条件中(该变量是可变的)
- 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)
synchronized
前面的volatile只能作用于变量,而且不能保证原子性
synchronized更强,可以使用在变量、方法、和类级别的,而且可以保证变量修改的可见性(当执行完synchronized之后,修改后的变量对其他的线程是可见的)和原子性(被保护的代码块是一次被执行的,没有任何线程会同时访问)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全
package com.company.Thread.Synchronized;
public class synchronizedTest {
public synchronized void test1(){}
public void test2(){
synchronized (this){}
};
}
锁机制:
同步代码块:
monitorenter:进入指令
monitorexit:退出指令
同步方法(JVM底层实现):方法修饰符上的ACC_SYNCHRONIZED实现
synchronized使用 (锁对象、类)
- 修饰普通方法
- 修饰代码块
- 修饰静态方法
package com.company.Thread.Synchronized;
public class synchronizedTest {
private Object object=new Object();
//修饰普通方法,锁synchronizedTest对象
public synchronized void test1(){
}
public void test2(){
//修饰代码块,也是锁对象,this是指本对象
synchronized (this){
}
//也可以锁设置的对象object
synchronized (object){
}
}
}
修饰普通方法、代码块(内置锁),本质上是锁对象,也就是堆内的对象实例,这些方式也称为客户端锁
客户端锁不赞成使用,会造成高耦合,建议使用设计模式里的装饰器模式
修饰静态方法有点不同
我们知道静态方法在类加载时也随之加载在方法区,它们不需要实例就可以使用,所以synchronized锁定的是类(类的字节码对象)
public static synchronized void test3(){
}
但是类锁和对象锁不会冲突
package com.company.Thread.Synchronized;
public class SynchronizedTest{
private Object object=new Object();
//修饰普通方法,锁synchronizedTest对象
public synchronized void test1(){
for (int i=0;i<5;i++){
System.out.println("对象锁:"+i);
}
}
public void test2(){
//修饰代码块,也是锁对象,this是指本对象
synchronized (this){
}
//也可以锁设置的对象object
synchronized (object){
}
}
public static synchronized void test3(){
for (int i=0;i<5;i++){
System.out.println("类锁:"+i);
}
}
public static void main(String[] args){
SynchronizedTest synchronizedTest=new SynchronizedTest();
Thread thread1=new Thread(()->
synchronizedTest.test1()
);
Thread thread2=new Thread(()->
test3()
);
thread1.start();
thread2.start();
}
}
synchronized锁的一些特点
内置锁的可重入性
同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入
package com.company.Thread.Synchronized;
public class Father {
public synchronized void test1(){
System.out.println("father类调用");
}
}
class Child extends Father{
public synchronized void test2(){
System.out.println("child类调用方法test2");
}
public synchronized void test3(){
System.out.println("child类调用方法test3");
test2();
super.test1();
}
}
class Test{
public static void main(String[] args){
Child child=new Child();
child.test3();
}
}
test3的对象锁可以重入
重入锁是怎么实现可重入性的,其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁
锁的释放
很简单,就两种情况,运行完了,释放
运行出异常,报错释放(为了预防死锁)
volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化