目录
1. 线程安全
为什么会出现线程安全的判断标准?
-
是否有多线程环境
-
是否有共享数据
-
是否有多条语句操作共享数据
前两点是创建线程的基础,解决线程安全只能阻止多条语句操作共享数据
1.1 线程不安全示例
多个线程对同一个共享数据进行访问,而不采取同步操作,操作结果是不一致的
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
结果总是小于1000
不采用同步操作出现的问题:详细解析参考该内容
-
可见性:CPU缓存引起
-
一个线程对共享变量的修改,另一个线程能够立即看到,Java提供了volatile关键字来保证可见性
当一个共享变量被volatile修饰时,它会保证修改的值会被立即更新到主存,当有其他线程需要读取时,它会去内存中读取新的值;普通共享变量不保证可见性是因为共享变量修改后刷新到主存的时间不确定,因此无法保证可见性
-
-
原子性:分时复用引起
- 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
只有语句1的操作具备原子性,即:Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,则通过synchronized和lock来实现x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中 y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。 x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。 x = x + 1; //语句4: 同语句3
- 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
-
有序性:重排序引起
- 即程序执行的顺序按照代码的先后顺序执行,volatile关键字可以保证一定的有序性,另外还可以通过synchronized和lock来保证有序性;JMM是通过Happens_Before规则来保证有序性
1.2 Java如何解决并发问题
对于这个问题,需要先了解Java内存模型(JMM)
JMM本质上可以理解为:Java内存模型,JVM如何提供按需禁用缓存和编译优化
具体方法如下:
-
volatile、synchronized和final三个关键字
-
Happens-Before规则
-
单一线程原则:在一个线程内,在程序前面的操作先行发生于后面的操作
-
管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
-
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
-
线程启动规则:Thread对象的start()方法调用先行发生于此线程的每一个动作
-
线程加入规则:Thread对象的结束先行发生于join()方法返回
-
线程中断规则:对现成interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过interrupted()方法检测到是否有中断发生
-
对象终结规则:一个对象的初始化完成(构造函数执行结束)线性发生于它的finalize()方法的开始
-
传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C
-
可见性、有序性和原子性的解决方案
2. 线程同步
使用三种方式创建线程,线程同步如何解决线程安全?
2.1 继承Thread–同步代码块:
package com.carl.javaadvanced.multithreading;
public class WindowsUp2 extends Thread{
private static int ticket=100;
private static Object obj=new Object();//解决obj不是唯一的就必须让obj变成静态的
@Override
public void run() {
while (true) {
//synchronized(obj) {//继承Thread我们发现,使用obj并不是唯一的一把锁,还是会出现线程安全问题
//能不能不创建对象就可以使用一个锁,和实现Runnable接口一样使用this关键字,答案是可以使用当前类的锁,只是不能使用this关键字
synchronized (this.getClass()){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public static void main(String[] args) {
WindowsUp2 wu1=new WindowsUp2();//只创建一个对象的时候,锁可以是一个类对象的成员变量,也可以是this,如果是继承Thread类,需要创建三个对象,锁就必须是静态的
WindowsUp2 wu2=new WindowsUp2();
WindowsUp2 wu3=new WindowsUp2();
wu1.start();
wu2.start();
wu3.start();
}
}
解析:
-
使用继承Thread类的方式,创建多个线程,需要将共享变量设置为static
-
同步监视器也必须是static的,或者不创建其他类对象,采用
this.getClass
或WindowsUp2.class
充当唯一类对象锁
2.2 继承Thread–同步方法:
package com.carl.javaadvanced.multithreading;
public class WindowsUp4 extends Thread{
private static int ticket=100;
@Override
public void run() {
boolean flag=true;
while (true) {
if (flag == true) {
flag=show();
}else{
break;
}
}
}
public static synchronized boolean show(){//这里的同步监视器是WindowsUp4
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
}else{
return false;
}
return true;
}
public static void main(String[] args) {
WindowsUp4 wu1=new WindowsUp4();
WindowsUp4 wu2=new WindowsUp4();
WindowsUp4 wu3=new WindowsUp4();
wu1.start();
wu2.start();
wu3.start();
}
}
解析:
当继承Thread类需要创建多个线程对象的情况下,同步方法必须声明为static静态的,此时同步监视器就是当前类
2.3 实现Runnable接口—采用同步代码块:
public class WindowsUp implements Runnable{
private int ticket=1000;
Object obj=new Object();
@Override
public void run() {
while (true) {
synchronized(obj) {//只要是共享一把锁,不管这个对象是什么,只要是一个相同的锁,可以是任意类对象
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public static void main(String[] args) {
WindowsUp wu=new WindowsUp();//只创建一个对象的时候,锁可以是一个类对象的成员变量,也可以是this,如果是继承Thread类,需要创建三个对象,锁就必须是静态的
Thread t1=new Thread(wu,"窗口1");
Thread t2=new Thread(wu,"窗口2");
Thread t3=new Thread(wu,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
解析:
-
由于实现Runnable接口只需要创建一次线程对象,因此,可以使用类类型的对象作为同步监视器,因为创建一次对象,成员变量是唯一的,但是我们发现实际上创建一个对象太麻烦了,我们是可以直接使用this关键字作为同步锁。
-
共享变量可以不是静态的,因为只创建了一次对象
2.4 实现Runnable接口—采用同步方法:
package com.carl.javaadvanced.multithreading;
public class WindowsUp3 implements Runnable{
private static int ticket=100;
@Override
public void run() {
boolean flag=true;
while (true) {
if (flag == true) {
flag=show();
}else{
break;
}
}
}
public synchronized boolean show(){//这里的同步监视器就是this
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:票号为:" + ticket);
ticket--;
}else{
return false;
}
return true;
}
public static void main(String[] args) {
WindowsUp3 wu1=new WindowsUp3();//只创建一个对象的时候,锁可以是一个类对象的成员变量,也可以是this,如果是继承Thread类,需要创建三个对象,锁就必须是静态的
Thread t1=new Thread(wu1);
Thread t2=new Thread(wu1);
Thread t3=new Thread(wu1);
t1.start();
t2.start();
t3.start();
}
}
解析:
实现Runnable接口由于只创建一次线程对象,同步方法默认的同步监视器是this
同步的优缺点
优点:解决了多线程的数据安全问题 弊端:当线程很多时,每个线程都会去判断同步上的锁,很耗费资源,并且降低了程序运行的效率
同步方法的锁
同步方法的锁是this,static修饰的同步方法的锁是字节码文件
3. 锁
Lock
JDK5.0新增的功能
Lock使用步骤:
-
创建ReentrantLock对象
-
在需要线程执行的内容开头调用lock()方法
-
在线程执行的内容结尾调用unlock方法
-
一般使用try调用lock()和线程执行内容,finally调用unlock方法