线程安全性
基本介绍
- 线程的合理使用能够提升程序的处理性能,主要有两个方面,第一个是能够利用多核 cpu 以及超线程技术来实现线程的并行执行;第二个是线程的异步化执行相比于同步执行来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量
- 但是多线程并行的同时带来线程安全性的问题:如何保证共享资源的正确性
- 举个例子:
- 将下面代码运行,让1000个线程去访问并且将共享资源加1,正常情况下,每次相加的数与顺序数是相等的,但是结果并非如此
public class Main {
//共享资源
private static int count = 0;
public static void add() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//对共享资源进行操作
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 1000; i++) {
new Thread(()->Main.add()).start();
Thread.sleep(1);
System.out.println("第"+i+"运行结果" + count);
}
}
}
- 结果会发现,有些线程取到的数据跟他本来要取得数据不对等,这种现象就是线程不安全。
- 所以可以总结一下,造成线程安全性问题有一下几个条件:
- 是否是共享资源:共享,是指这个数据变量可以被多个线程访问
- 共享资源的状态是否是可变的:可变,指 这个变量的值在它的生命周期内是可以改变的 。
- 判断这个线程是否是安全的,就是判断在多个线程访问下,在不需额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态依然是正确的,这里的正确性是指这个对象的结果预期跟我们事先预料到的结果是保持一致。
如何解决线程安全性问题
- 解决这个问题的本质就是在于如何将线程的并行改变成串行就可以。
- 所以我们可以采用同步方式去解决这个问题,也就是通过锁来解决线程安全性的问题。
- java提供的锁就是Synchroinzed 关键字。
synchronized
基本认识
- 有三种加锁的方式:
- 修饰实例方法
- 修饰静态方法
- 修饰同步代码块
- 两种作用范围:
- 对象锁
- 类锁
修饰实例方法
- 就是在方法上加上 synchronized 关键字修饰
- 这里举一个例子,当没有加方法修饰的时候
public class Main implements Runnable{
static Object object = new Object();
//共享资源
private static int count = 0;
public void add() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
Thread thread1 = new Thread(main);
Thread thread2 = new Thread(main);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
}
}
- 结果:本来是Thread-0先完成蓝色框两个方法后才能到Thread-1这两个方法,所以这个方法是线程不安全的
- 加上 synchronized 关键字修饰之后
public class Main implements Runnable{
static Object object = new Object();
//共享资源
private static int count = 0;
public synchronized void add() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
Thread thread1 = new Thread(main);
Thread thread2 = new Thread(main);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
}
}
- 结果:完成上一个不能完成的预期效果,这个方法就是线程安全的。
- 总结:方法锁是锁的是this对象,也就是说我们在方法锁里面synchronized其实锁的就是当前this对象。
- 但是如何去验证这个锁锁的对象就是this对象呢,看接下来
public class Main implements Runnable{
static Object object = new Object();
//共享资源
private static int count = 0;
public synchronized void add() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了add方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void pass() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了pass方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
Thread thread1 = new Thread(main);
Thread thread2 = new Thread(main);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
pass();
}
}
- 结果:从结果来看,无论是add方法还是pass方法,同一时刻只能有一个线程进入,这就是this锁导致的。
修饰静态方法
- 如果再加上static修饰变成静态方法的话,那作用范围就不一样了,是作用于类上
- 举个例子:
public class Main implements Runnable{
static Object object = new Object();
//共享资源
private static int count = 0;
public synchronized void add() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了add方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void pass() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了pass方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main1 = new Main();
Main main2 = new Main();
Thread thread1 = new Thread(main1);
Thread thread2 = new Thread(main2);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
pass();
}
}
- 结果:每个线程不同的对象就完全没有锁的效果了
- 但是如果加上static就不一样了
public class Main implements Runnable{
static Object object = new Object();
//共享资源
private static int count = 0;
public static synchronized void add() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了add方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void pass() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了pass方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main1 = new Main();
Main main2 = new Main();
Thread thread1 = new Thread(main1);
Thread thread2 = new Thread(main2);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
pass();
}
}
- 结果:两个不同对象的线程运行各自对象的方法还是按照有锁的时候进行,说明加上static作用范围是整个类而不是单单止对象了。
修饰同步代码块
- 简而言之就是给代码块上锁
- 上面那个例子在共享变量上面加上同步代码块
public class Main {
static Object object = new Object();
//共享资源
private static int count = 0;
public static void add() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//对共享资源进行操作
//修饰同步代码块
synchronized (object) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 1000; i++) {
new Thread(()->Main.add()).start();
Thread.sleep(1);
}
System.out.println("运行结果" + count);
}
}
- 结果:刚好是1000说明线程安全
- 总结:在这个例子中,我们使用了synchronized锁住了run方法中的代码块。表示同一时刻只有一个线程能够进入代码块。同步代码块锁主要是对代码块进行加锁,此时同一时刻只能有一个线程获取到该资源,要注意每一把锁只负责当前的代码块,其他的代码块不管。
- 但是如果 synchronized (class) 中放的是本类的class,那作用范围会由对象变成类
- 举个例子
public class Main implements Runnable{
//共享资源
private static int count = 0;
public void add() {
synchronized (new Object()) {
try {
System.out.println(Thread.currentThread().getName() + "进入到了add方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void pass() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了pass方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main1 = new Main();
Main main2 = new Main();
Thread thread1 = new Thread(main1);
Thread thread2 = new Thread(main2);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
}
}
- 结果:两个不同的对象各自进入各自的方法中,没有形成锁的效应
- 但如果加入本类的class就不一样了
public class Main implements Runnable{
//共享资源
private static int count = 0;
public void add() {
synchronized (Main.class) {
try {
System.out.println(Thread.currentThread().getName() + "进入到了add方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void pass() {
try {
System.out.println(Thread.currentThread().getName() + "进入到了pass方法");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "执行完毕" + "释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Main main1 = new Main();
Main main2 = new Main();
Thread thread1 = new Thread(main1);
Thread thread2 = new Thread(main2);
thread1.start();
thread2.start();
// 线程1和线程2只要有一个还存活就一直执行
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("main程序运行结束");
}
@Override
public void run() {
add();
}
}
- 结果:方法会依次执行虽然是不同的对象,但是类是相同的。
锁的升级(还在理解中)
-
首先先思考一下要实现多线程的互斥性需要哪些因素?
- 第一肯定需要保存锁的状态
- 第二这个状态需要对多个线程共享
-
而 synchronized 锁是如何存储的呢?
- 答案在于 synchronized ( 是基于lock ) 这个对象的生命周期来控制锁粒度的。
-
然而这个lock对象的跟锁的存储有什么关系呢,接下来就来分析
-
首先先看对象在内存中的布局
wait,notify,notifyAll
-
wait/notify/notifyall 基本概念:
-
wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态 。
-
notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤醒某个竞争该对象锁的线程 X 。 线程 A synchronized 代码 执行结束并且释放了锁之后 ,线程 X 直接获得对象锁权限,其他竞争线程继续等待 即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用 。
-
notifyAll:notifyall 和 notify 的区别在于, notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程 A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权限
-
需要注意的是:三个方法都必须在三个方法都必须在synchronized 同步关键所限定的作用域中调用,否则会报错字 java.lang.IllegalMonitorStateException ,意思是因为没有,所以线程对对象锁的状态是不确定的,不能调用这些方法。另外,通过同步机制来确保线程从wait方法返回时能够感知到notify线程对变量做出的改变。
-
举个例子:
public class ThreadA extends Thread{
private Object lock;
public ThreadA(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("start ThreadA");
try {
lock.wait(); //实现线程的阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end ThreadA");
}
}
}
public class ThreadB extends Thread{
private Object lock=new Object();
public ThreadB(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("start ThreadB");
lock.notify(); //唤醒被阻塞的线程
System.out.println("end ThreadB");
}
}
}
public class WaitNotifyDemo {
public static void main(String[] args) {
Object lock=new Object();
ThreadA threadA=new ThreadA(lock);
threadA.start();
ThreadB threadB=new ThreadB(lock);
threadB.start();
}
}
-
结果:threadA在运行之后,被wait()方法等待了,然后将锁给了ThreadB,B执行中将A唤醒,然后B执行完之后把锁给了A。
-
流程图: