三、并发安全
线程间的共享
一、synchronized 内置锁
Java语言的关键字
作用:多个线程在同一时刻只能有一个线程进入这个方法或者代码块中。可以保证线程对于变量或者属性的原子性和可见性、排他性。
用处:作用于代码块或者方法上,进行修饰
synchronized关键字本质上是把对象做了一把锁,在代码块中需要对当前对象进行加锁,在方法上加锁缺省是对这个类的当前实例加锁,所以synchronized加的锁不是在方法上也不是在代码块上,本质上是在类的当前实例上。
synchronized关键字锁的是对象,锁的对象不同,线程就可以并行地执行
二、对象锁
首先先来运行两段代码
代码1
package com.tinner.thread;
/**
* @Author Tinner
* @create 2019/9/20 17:33
*/
public class DiffObj {
private static class Obj1 implements Runnable{
private DiffObj diffObj;
public Obj1(DiffObj diffObj) {
this.diffObj = diffObj;
}
@Override
public void run() {
System.out.println("TestObj1 is running ...." + diffObj);
diffObj.instance();
}
}
private static class Obj2 implements Runnable{
private DiffObj diffObj;
public Obj2(DiffObj diffObj) {
this.diffObj = diffObj;
}
@Override
public void run() {
System.out.println("TestObj2 is running ...." + diffObj);
diffObj.instance2();
}
}
private synchronized void instance(){
try {
Thread.sleep(3000);
System.out.println("synInstance1 is going..." + this.toString());
Thread.sleep(3000);
System.out.println("synInstance1 ended + " + this.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void instance2(){
try {
Thread.sleep(3000);
System.out.println("synInstance2 is going..." + this.toString());
Thread.sleep(3000);
System.out.println("synInstance2 ended + " + this.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
DiffObj instance1 = new DiffObj();
Thread t3 = new Thread(new Obj2(instance1));
DiffObj instance2 = new DiffObj();
Thread t4 = new Thread(new Obj1(instance1));
//Thread t4 = new Thread(new Obj1(instance2));
t3.start();
t4.start();
Thread.sleep(1000);
}
}
运行结果1
同一实例代码2
修改main方法
public static void main(String[] args) throws InterruptedException {
DiffObj instance1 = new DiffObj();
Thread t3 = new Thread(new Obj2(instance1));
DiffObj instance2 = new DiffObj();
//Thread t4 = new Thread(new Obj1(instance1));
Thread t4 = new Thread(new Obj1(instance2));
t3.start();
t4.start();
Thread.sleep(1000);
}
运行结果2
不同实例总结
比较两个运行结果,可以发现第一个方式是第一个线程执行完毕之后才执行第二个线程;第二个方式基本上是两个线程同时开始同时结束的。锁的实例不一样,也是可以并行的,只有两个线程锁的对象的实例是相同的时候,才能达到synchronized 那种效果。
三、类锁
将synchronized关键字加在一个类的static方法上的时候,才算是一个类锁。
类锁和对象锁之间还是可以相互的并行执行的。
代码
/**
*类说明:演示实例锁和类锁是不同的,两者可以并行
*/
public class InstanceAndClass {
private static class SynClass extends Thread{
@Override
public void run() {
System.out.println("TestClass is running...");
synClass();
}
}
private static class ObjSyn implements Runnable{
private InstanceAndClass SynClassAndInstance;
public ObjSyn(InstanceAndClass SynClassAndInstance) {
this.SynClassAndInstance = SynClassAndInstance;
}
@Override
public void run() {
System.out.println("TestInstance is running..."+SynClassAndInstance);
SynClassAndInstance.instance();
}
}
//实例方法
private synchronized void instance(){
SleepTools.second(1);
System.out.println("synInstance is going..."+this.toString());
SleepTools.second(1);
System.out.println("synInstance ended "+this.toString());
}
//静态方法
private static synchronized void synClass(){
SleepTools.second(1);
System.out.println("synClass going...");
SleepTools.second(1);
System.out.println("synClass end");
}
public static void main(String[] args) {
InstanceAndClass synClassAndInstance = new InstanceAndClass();
Thread t1 = new SynClass();
Thread t2 = new Thread(new ObjSyn(synClassAndInstance));
t2.start();
SleepTools.second(1);
t1.start();
}
}
运行结果
类锁可以发现,这两个进程是可以并行执行的。
注意:
synchronized关键字只能锁对象,但是在类锁中,synchronized关键字明明用到了static方面,那么它锁的是这个class对象。当我们需要创建一个对象的实例的时候,虚拟机会进行一个类加载的过程,每一个类在虚拟机里面都有一个唯一的class对象。synchronized本质上锁的是每个类所独有的class对象。
那么就可以知道,instance方法锁的是实例对象,synClass方法锁的是类的对象,本质上他们锁的也是两个完全不同的对象,所以可以并行执行。
结论
从严格意义上来讲,类锁只是一个概念上的东西,并不是真实存在的。本质上锁的是类的class对象。而且类锁和对象锁之间也是互不干扰的。
四、volatile关键字,最轻量的同步机制
保证了变量的可见性。但是并没有提供变量的原子性,不能够保证复杂计算的时候数据的正确性。
代码1
/**
* 类说明:演示Volatile的提供的可见性
*/
public class VolatileCase {
private static boolean ready;
private static int number;
//
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready);//无限循环
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
运行这段代码,可以看到内存飙升,程序根本停不下来,原因就是主方法中修改了ready变量的值之后,在线程中检测不到ready的变化,所以程序会一直运行下去
稍作修改
当我们给ready变量加一个volatile关键字之后
private static volatile boolean ready;
可以看到系统正常运行了,正常停止
volatile关键字作用
题外话
当我们不加volatile关键字,而是在无限循环中去加入一条打印语句的时候,看代码:
public class VolatileCase {
private static boolean ready;
private static int number;
//
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready){
//无限循环
System.out.println("jinping");
}
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
运行结果
i题外话运行结果可以看到程序正常的停止了。
为什么会出现这种现象呢?我并没有加volatile关键字啊
我们仅仅在无限循环中加了一个普通的打印语句,这个打印语句中牵涉到了synchronized关键字的内存语义。
image.png
说起来就要扯到JMM的内存模型中去了,synchronized关键字在内存语义上面强制要求把共享变量刷回到主内存,以及强制将使用的这个变量读到当前工作的内存上去。
最常见的适用场景:一个线程写,多个线程读
五、什么是线程安全?怎么才能做到线程安全?
什么是线程安全
如果说有多个线程访问同一个类的实例的时候,不管运行环境如何,我们的类的实例都能表现出正确的预期结果及行为,这就是线程安全。
实现线程安全的方式
其实本质上线程安全就是解决“修改--共享变量”的问题
-
栈封闭(线程封闭)
栈封闭就是让变量或者实例变得不可共享。在我们运行任何一个线程的时候,JDK都会为每一个线程分配一个栈,栈和程序计数器是每一个线程所独有的,其他线程看不到。堆和方法区是线程之间共享的。既然栈是每个线程所独有的,那么将栈封闭,里面的变量就不会被共享。比如:局部变量。
实际开发过程中,多使用局部变量,少使用全局变量。 -
无状态的类
Java语言中,没有成员变量的类,就是无状态的类,只有方法。那么这种类就是线程安全的。 -
让类不可变
让这个类的所有的属性加final关键字。但是如果这个类中的成员变量中是个对象的话,这个属性还是不安全的。因为虽然final关键字修饰了实体类,但是它修饰的只是这个实体类的引用(修饰了之后引用不可变)。但是不代表这个类在堆上的实例的内容不可变。
不可变类
还有一种只提供读的方法,不提供写的方法,外面写不动。
/**
* 类不可变--事实不可变
*/
public class ImmutableClassToo {
private final List<Integer> list = Arrays.asList(1,2,3);
public boolean isContain(int i){
return list.contains(i);
}
}
-
volatile
如果只是简单的set和get方法,可以使用该关键字。但是如果是++等操作,不适用。 - 加锁和CAS
-
安全的发布
写下代码自己体会
/**
* 不安全的发布
*/
public class UnsafePublish {
private List<Integer> list = new ArrayList<>(3);
public UnsafePublish() {
list.add(1);
list.add(2);
list.add(3);
}
public List getList() {
return list;
}
public static void main(String[] args) {
UnsafePublish unSafePublish = new UnsafePublish();
List<Integer> list = unSafePublish.getList();
System.out.println(list);
list.add(4);
System.out.println(list);
System.out.println(unSafePublish.getList());
}
}
/**
* 安全的发布
*/
public class SafePublishToo {
private List<Object> list
= Collections.synchronizedList(new ArrayList<>(3));
public SafePublishToo() {
list.add(1);
list.add(2);
list.add(3);
}
public List getList() {
return list;
}
public static void main(String[] args) {
SafePublishToo safePublishToo = new SafePublishToo();
List<Integer> list = safePublishToo.getList();
System.out.println(list);
list.add(4);
System.out.println(list);
System.out.println(safePublishToo.getList());
}
}
Collections.synchronizedList()
包装好的线程安全的。
自己封装
/**
* 仿Collections对容器的包装,将内部成员对象进行线程安全包装
*/
public class SoftPublicUser {
private final UserVo user;
public UserVo getUser() {
return user;
}
public SoftPublicUser(UserVo user) {
this.user = new SynUser(user);
}
private static class SynUser extends UserVo{
private final UserVo userVo;
private final Object lock = new Object();
public SynUser(UserVo userVo){
this.userVo = userVo;
}
public int getAge() {
synchronized (lock){
return userVo.getAge();
}
}
public void setAge(int age) {
synchronized (lock){
userVo.setAge(age);
}
}
}
}
- ThreadLocal(线程封闭)