目录
5.3synchronized也可以对代码重排序增加一定的约束
1.4tryLock(long time, TimeUnit unit)方法
一、线程安全(Thread Safe)
代码运行的结果必须是100%符合预期
1.线程不安全出现的原因
1.1没有保证原子性
在开发者眼中:多个线程之间操作同一块数据,至少有一个线程在修改该数据
在系统眼中:Java代码中的一条语句肯对应的是多条指令
线程调度是可能在任意时刻发生,但是不会切割指令(一条指令只有两种情况,执行完和完全没有执行)
线程调度可能发生在这四个阶段
但是在我们程序员眼中r++是一个原子性的操作(要么执行完成,要么执行失败),但是实际的执行是保证不了原子性的
线程不安全的常见原因-----原子性被破坏
1.2 内存可见性问题,导致某些线程读取到脏数据
指令的执行速度 >> 内存的读写速度
CPU为了提升数据获取速度,解决读写速度不一样,一般在CPU中设置缓存
JVM规定了JVM内存模型,把一个线程想象成一个CPU
主内存:实际内存的模拟
工作内存:CPU中缓存的模拟
线程中所有的数据操作需要
1.从主内存加载到工作内存中
2.在工作内存中处理
3.完成最终的处理之后,再把数据同步到主内存中
内存可见性:一个线程对数据的操作,很可能其他线程是无法感受到的,甚至再某些情况下,会被优化成完全看不到的结果
1.3代码重排序导致的问题
我们写的程序,也是经过中间很多环节优化的结果,并不能保证最终指向的语言和我们写的语句是一模一样的,比如:删除某些步骤/调整某些步骤的执行顺序;但是我们是不知道哪里做了优化
代码重排序:执行的指令和书写的指令不一致
基本原则:JVM要求不管怎么优化,对于单线程的视角,结果不能发生改变;但是由于没有规定多线程环境的情况,导致可能在多线程环境下出现问题
1.4一些安全/不安全的线程
ArrayList、LinkedList、Priority、TreeMap、TreeSet、HashMap、HashSet、StringBuilder都不是线程安全的
Vector、Stack、Dictionary、StringBuffer都是线程安全的
2.什么时候不需要考虑线程安全问题
多个线程之间没有任何数据共享的情况下,是线程安全的
多个线程之间存在数据共享,但是没有修改(写)操作时,是线程安全的
二、synchronize锁-----保护线程安全
- 当多个线程都有加锁操作并且申请的是同一把锁时,临界区的代码是互斥进行的
- 根据加锁的原理,只会有一个线程可以持有锁,其余没有抢到锁的线程就会进入到该锁的阻塞队列,放弃CPU,临界区的代码必须在持有锁的前提下才能执行
- 互斥的必要条件:线程都有加锁操作,是同一把锁,锁的是同一个对象
1.synchronize修饰方法(普通方法/静态方法)
- 修饰普通方法 -> 对当前对象加锁
- 修饰静态方法 -> 对当前的类加锁
synchronize 方法类型 方法名称(...){
...
}
2.synchronize修饰同步代码块
synchronize(){
...
}
public class Main {
// sync 修饰普通方法
public synchronized int add() {
return 0;
}
// sync 修饰静态方法
private synchronized static int sub() {
return 0;
}
// sync 同步代码块
public void method() {
Object o = new Object();
synchronized (o) {
}
}
// sync 同步代码块
public void method2() {
synchronized (Main.class) {
}
}
}
3.有无static加锁对象的区别
和static有关的只有一个类加锁
4.synchronized加锁粗粒度的问题
- 粗略的来说,加锁粒度越细,并发的可能性越高;加锁粒度越粗,临界区代码的执行时间会变长
- 但是粒度没有说哪个更好,最好值是需要经过工程测量的
5.synchronize的作用
5.1保证原子性
通过正确加锁使需要保证原子性的代码互斥来实现
5.2在有限程度上保证内存可见性
临界区期间的数据读写是不保证的
1.可能读到的数据再次被别的线程更改之后,就看不到了
2.期间是否有数据同步回主内存
5.3synchronized也可以对代码重排序增加一定的约束
三、Lock锁-----更灵活
将unlock()放在finally中,确保任何时候都可以解锁
1.Lock中几种常见的方法
1.1lock()方法
如果锁已被其他线程获取,则进行等待
- 主线程调用 t.jion()等子线程结束
- 子线程调用lock.lock()等主线程释放锁,陷入死循环
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
lock.lock();
System.out.println("子线程进入临界区"); // 是一直不会输出的
// 主线程持有锁,子线程不能加锁
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
t.join();
}
}
1.2lockInterruptibly()方法
能够中断等待获取锁的线程
- 当一个线程获取了锁之后,是不会被interrupt()方法中断的。调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程
- lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的
- 只使用interrupt()方法是没有输出的
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
lock.lock();
System.out.println("子线程进入临界区"); // 是一直不会输出的
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt(); // 尝试让子线程停下来,但实际是不会停止的
t.join();
}
}
- 加上lockInterruptibly()方法就可以中断了
- 在请求锁过程中,由于线程被中止了,导致抛出异常就会执行catch中的输出
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
try {
lock.lockInterruptibly();
System.out.println("子线程进入临界区"); // 理论上,这句代码永远到达不了
} catch (InterruptedException e) {
System.out.println("收到停止信号,停止运行"); // 输出这句话,说明此时子线程停止了
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt(); // 尝试让子线程停下来
t.join();
}
}
1.3tryLock()方法
如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
boolean b = lock.tryLock();
if (b == true) {
// 加锁成功了
System.out.println("加锁成功");
System.out.println("子线程进入临界区"); // 不能输出
} else {
System.out.println("加锁失败"); // 输出加锁失败
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt(); // 尝试让子线程停下来
t.join();
}
}
1.4tryLock(long time, TimeUnit unit)方法
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
boolean b = false;
try {
// 等待5s ,如果拿到锁返回true,拿不到锁返回false
b = lock.tryLock(5, TimeUnit.SECONDS);
if (b == true) {
// 加锁成功了
System.out.println("加锁成功");
System.out.println("子线程进入临界区"); // 不会输出
} else {
System.out.println("5s 之后加锁失败");
}
} catch (InterruptedException e) {
System.out.println("被打断了"); // 输出 被打断了
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt(); // 尝试让子线程停下来
t.join();
}
}
1.5unlock()方法-----释放锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author happy
*/
public class Main {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
boolean b = false;
try {
b = lock.tryLock(5, TimeUnit.SECONDS);
if (b == true) {
// 加锁成功了
System.out.println("加锁成功"); // 输出 加锁成功
System.out.println("子线程进入临界区"); // 输出 子线程进入临界区
} else {
System.out.println("5s 之后加锁失败");
}
} catch (InterruptedException e) {
System.out.println("被打断了");
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
// 释放锁,此时子线程可以拿到锁
lock.unlock();
t.join();
}
}
四、synchronized锁和JUC下的Lock锁的区别
1.synchronized锁
- 不需要人为写释放锁
- 只有一种类型的锁
- 只有一直请求锁
2.Lock锁
- 可能忘记写lock.unlock();导致锁不能释放
- 书写比较灵活,可以在一个方法中加锁,在另外一个方法中解锁
- 锁的类型更灵活(公平锁/非公平锁 、 读写锁/独占锁 、 可重入锁/不可重用锁)
- 加锁策略更灵活(一直请求锁、带中断的请求锁、带超时的请求锁)