1. 线程同步锁
- 在多线程并发控制,当多个线程同时操作一个可共享的资源时,如果没有采取同步机制,将会导致数据不准确,因此需要加入同步锁,确保在该线程没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。
- 例如典型的卖火车票问题,多个窗口(线程)同时卖火车票(数据),不使用线程同步的话就会造成同一张票卖多次的情况
public class ThreadSync {
private static int mTickets = 100;
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "窗口1");
Thread thread2 = new Thread(new MyRunnable(), "窗口2");
Thread thread3 = new Thread(new MyRunnable(), "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
private static void sellTicket() {
while (mTickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + mTickets + "张票");
mTickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
sellTicket();
}
}
}
某次运行结果:
窗口1卖第100张票
窗口2卖第99张票
窗口3卖第98张票
窗口3卖第97张票
窗口1卖第96张票
窗口2卖第97张票
窗口3卖第95张票
窗口1卖第94张票
窗口2卖第93张票
窗口2卖第92张票
窗口3卖第91张票
窗口1卖第90张票
窗口1卖第89张票
窗口3卖第89张票
- 从运行结果上看有可能存在多个窗口卖出了同一张票(如上面的97和89),这就是典型的线程不安全造成的,因而就涉及到了需要加锁使得线程同步,当一个线程开始买这张票时就加锁不允许其他线程进入卖这张票
1.1 同步的常见方式
synchronized修饰同步代码块或方法
Lock锁:常用的实现类有ReentrantLock,lock()获得锁,unlock()释放锁
volatile修饰变量
使用原子变量:如AtomicInteger、AtomicBoolean、AtomicLong等
阻塞队列:常用的有LinkedBlockingQueue(生产者消费者模型)
2. 同步锁的种类
2.1 synchronized内置锁
- 每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。
内置锁也是一个互斥锁,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将一直等待下去
- Synchonized关键字可作用于
方法和代码块中;加锁的类型包括类锁和对象锁
- Synchonized是
隐式锁,无需手动写代码去获取锁和释放锁
2.1.1 类锁
- 类锁是作用于类的静态方法或者一个类的class对象上
- 例如如上面的卖票问题,就可以使用synchronized加类锁,使得最终呈现正确的结果
private synchronized static void sellTicket() {
while (mTickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + mTickets + "张票");
mTickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void sellTicket() {
synchronized (ThreadSync.class) {
while (mTickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + mTickets + "张票");
mTickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
窗口1正在卖第100张票
窗口1正在卖第99张票
窗口1正在卖第98张票
窗口1正在卖第97张票
...
窗口1正在卖第2张票
窗口1正在卖第1张票
2.1.2 对象锁
- 对象锁是用于对象实例方法,或者一个对象实例上
- 创建两个类锁的方法和两个对象锁的方法
public synchronized static void classLock1() {
System.out.println(Thread.currentThread().getName() + ", 开始运行,并获得ThreadLock类锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ", 结束运行,并释放ThreadLock类锁");
}
public static void classLock2() {
synchronized (ThreadLock.class) {
System.out.println(Thread.currentThread().getName() + ", 开始运行,并获得ThreadLock类锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ", 结束运行,并释放ThreadLock类锁");
}
}
public synchronized void objectLock1() {
System.out.println(Thread.currentThread().getName() + ", 开始运行,并获得ThreadLock对象锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ", 结束运行,并释放ThreadLock对象锁");
}
public void objectLock2() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ", 开始运行,并获得ThreadLock对象锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ", 结束运行,并释放ThreadLock对象锁");
}
}
class MyThreadA extends Thread {
private final ThreadLock threadLock;
public MyThreadA(String name, ThreadLock threadLock) {
super(name);
this.threadLock = threadLock;
}
@Override
public void run() {
super.run();
threadLock.classLock1();
}
}
class MyThreadB extends Thread {
private final ThreadLock threadLock;
public MyThreadB(String name, ThreadLock threadLock) {
super(name);
this.threadLock = threadLock;
}
@Override
public void run() {
super.run();
threadLock.classLock2();
}
}
class MyThreadC extends Thread {
private final ThreadLock threadLock;
public MyThreadC(String name, ThreadLock threadLock) {
super(name);
this.threadLock = threadLock;
}
@Override
public void run() {
super.run();
threadLock.objectLock1();
}
}
class MyThreadD extends Thread {
private final ThreadLock threadLock;
public MyThreadD(String name, ThreadLock threadLock) {
super(name);
this.threadLock = threadLock;
}
@Override
public void run() {
super.run();
threadLock.objectLock2();
}
}
public class ThreadLock {
public static void main(String[] args) {
Thread thread1 = new MyThreadA("类锁线程1", new ThreadLock());
Thread thread2 = new MyThreadB("类锁线程2", new ThreadLock());
Thread thread3 = new MyThreadC("对象锁线程1", new ThreadLock());
Thread thread4 = new MyThreadD("对象锁线程2", new ThreadLock());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果:
类锁线程1, 开始运行,并获得ThreadLock类锁
对象锁线程1, 开始运行,并获得ThreadLock对象锁
对象锁线程2, 开始运行,并获得ThreadLock对象锁
对象锁线程2, 结束运行,并释放ThreadLock对象锁
对象锁线程1, 结束运行,并释放ThreadLock对象锁
类锁线程1, 结束运行,并释放ThreadLock类锁
类锁线程2, 开始运行,并获得ThreadLock类锁
类锁线程2, 结束运行,并释放ThreadLock类锁
- 可以看出由于两个类锁都是ThreadLock类锁,即同一个锁,所以两个类锁是按顺序执行,即同步的。而两个对象锁和类锁不是同一种锁,所以对象锁与类锁间是异步执行的
- 虽然objectLock1和objectLock2都是对象锁,当上面锁的是两个不同的ThreadLock对象,所以他们之间也是异步的,当锁同一个对象时两者便会是同步的,如下面的例子
public class ThreadLock {
public static void main(String[] args) {
Thread thread1 = new MyThreadA("类锁线程1", new ThreadLock());
Thread thread2 = new MyThreadB("类锁线程2", new ThreadLock());
ThreadLock threadLock = new ThreadLock();
Thread thread3 = new MyThreadC("对象锁线程1", threadLock);
Thread thread4 = new MyThreadD("对象锁线程2", threadLock);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果:
类锁线程1, 开始运行,并获得ThreadLock类锁
对象锁线程1, 开始运行,并获得ThreadLock对象锁
对象锁线程1, 结束运行,并释放ThreadLock对象锁
对象锁线程2, 开始运行,并获得ThreadLock对象锁
类锁线程1, 结束运行,并释放ThreadLock类锁
类锁线程2, 开始运行,并获得ThreadLock类锁
对象锁线程2, 结束运行,并释放ThreadLock对象锁
类锁线程2, 结束运行,并释放ThreadLock类锁
- 由上所示,当对象锁锁的是同一个ThreadLock对象时,两个对象锁之间的执行就是同步的
2.1.3 synchronized原理
- 参考:www.cnblogs.com/little-shee…
- 使用javap -l -c -v 命令反编译class文件
同步代码块使用的是指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
synchronized修饰的方法并没有monitorenter和monitorexit指令。而是用ACC_SYNCHRONIZED的flag标记该方法是否是同步方法,从而执行相应的同步调用。
2.1.4 synchronized锁升级过程
- 偏向锁
- 场景:不存在或很少资源竞争的情况
- 优点:加锁和解锁不需要额外消耗,与执行非同步代码差距很小
- 缺点:如果线程间存在竞争就会带来额外的锁撤销的消耗
- 轻量级锁
- 场景:存在线程竞争,但同步块执行周期比较短
- 优点:竞争的线程不会阻塞,提高了相应速度
- 缺点:线程如果始终得不到锁,会使用自旋消耗CPU(自旋是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环)
- 重量级锁
- 场景:同步块执行时间长
- 优点:线程竞争不适用自旋,不会消耗CPU
- 缺点:线程阻塞,相应时间长
2.2 Lock锁
- Lock是一个接口,常用的实现类为ReentrantLock
- Lock是一种
显示锁,需要调用者手动获取锁lock.lock()和释放锁lock.unlock()
2.2.1 ReentrantLock
- ReentrantLock:可重入锁,所谓可
重入锁是指如果同一个实例的话可重新进入锁,例如:下面代码中递归调用并不会因为第一次执行add未执行完释放锁,继续内部调用add而造成锁死,因为ReentrantLock是一个可重入锁,当然synchronized也是一个可重入锁(可自行测试)
private int count = 0;
private void add() {
mLock.lock();
try {
count++;
System.out.print(count + " ");
if (count < 10) {
add();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
执行结果:
1 2 3 4 5 6 7 8 9 10
2.2.2 ReentrantReadWriteLock读写锁
- 当需要大量的读写操作时,使用读写锁比synchronized性能更好
- 如下所示,在set上加写锁,get上加读锁比set/get方法上加synchronized实测效率更高
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
@Override
public String getName() {
try {
readLock.lock();
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return name;
}
@Override
public void setName(String name) {
try {
writeLock.lock();
Thread.sleep(10);
this.name = name;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
@Override
public int getAge() {
try {
readLock.lock();
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return age;
}
2.3 volatile关键字
- volatile是Java虚拟机提供的轻量级的同步机制。在某些情况下,比锁要更加方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的
- 特性:
可见性、有序性(禁止指令重排)、不保证原子性
- 可见性: 当写一个 volatile 变量时。JVM 会把该线程对应的工作内存的共享变量刷新到主内存中去,使得多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 有序性(禁止指令重排): 通过插入内存屏障保证程序执行的顺序是按照代码的先后顺序依次执行(这也是单例模式中单例对象使用volatile修饰的原因)
- 不保证原子性: 原子性就是指一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,正是由于volatile这个特性,所以volatile无法实现一些非原子性的同步(如上面卖票中的mTickets--,--操作不是原子操作)
public class VolatileDemo {
private boolean mFlag = true;
private void task1() {
new Thread(() -> {
while (mFlag) {
}
}).start();
}
private void task2() {
new Thread(() -> {
try {
Thread.sleep(3000);
mFlag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
volatileDemo.task1();
volatileDemo.task2();
}
}
执行结果:
当不使用volatile关键字时,3s后程序仍然继续运行
当使用volatile关键字时,3s后程序运行结束
2.4 使用原子变量
- 原子变量主要是用来解决多线程下变量的原子性的问题,是jdk1.5后
java.util.concureent.atomic包提供的,常见的原子变量有AtomicInteger、AtomicBoolean、AtomicLong等
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
static final AtomicDemo mAtomicDemo = new AtomicDemo();
private static final AtomicInteger atomicInteger = new AtomicInteger(100000);
private static int i = 100000;
private void reduce() {
atomicInteger.decrementAndGet();
i--;
}
public void execute() {
ExecutorService executorService = Executors.newFixedThreadPool(50);
for (int i = 0; i < 100; i++) {
executorService.execute(new MyThread());
}
executorService.shutdown();
while (true) {
if (executorService.isTerminated()) {
System.out.println("Atomic方式执行完毕,结果为" + atomicInteger);
System.out.println("普通方式执行完毕,结果为" + i);
break;
}
}
}
static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
mAtomicDemo.reduce();
}
}
}
public static void main(String[] args) {
mAtomicDemo.execute();
}
}
执行结果:
Atomic方式执行完毕,结果为0
普通方式执行完毕,结果为2849
2.5 阻塞队列
- 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素
- 线程池的实现其实就是借助了阻塞队列,我们也可以用阻塞队列实现生产者-消费者模式
2.5.1 阻塞队列的种类
- 阻塞队列提供了一个公共的接口,jdk7提供了7种不同的实现类,即7种阻塞队列,包括:
- ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
2.5.2 阻塞队列实现生产者-消费者模式
public class Product {
private String name;
public Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class ProduceConsume {
private static final BlockingQueue<Product> mQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
Producer p = new Producer();
Consumer c1 = new Consumer();
Consumer c2 = new Consumer();
new Thread(p).start();
new Thread(c1, "消费者1").start();
new Thread(c2, "消费者2").start();
}
static class Producer implements Runnable {
public void run() {
try {
while (true) {
mQueue.put(produce());
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
Product produce() throws InterruptedException {
Thread.sleep(500);
String name = "产品" + new Random().nextInt(100);
System.out.println("生产者生产:" + name + ",产品个数为" + mQueue.size());
return new Product(name);
}
}
static class Consumer implements Runnable {
public void run() {
try {
while (true) {
consume(mQueue.take());
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
void consume(Product product) throws InterruptedException {
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + "消费:" + product.getName());
}
}
}
执行结果:
...
生产者生产:产品84,产品个数为9
生产者生产:产品24,产品个数为10
消费者1消费:产品10
消费者2消费:产品94
生产者生产:产品82,产品个数为9
生产者生产:产品3,产品个数为10
消费者1消费:产品42
消费者2消费:产品44
...
3. synchronized与lock加锁的区别
- synchronized是关键字属于jvm层面的锁,Lock是接口(常用实现类是ReentrantLock)属于api层面的锁
- synchronized在线程发生异常时会自动释放锁,因此不会发生异常
死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Lock是可以中断锁(unlock),Synchronized是非中断锁,必须等待线程执行完成才能释放锁。
- Lock可以使用读锁提高多线程读效率,写锁提高多线程写效率(ReentrantReadWriteLock)
- synchronized是
非公平锁,而Lock锁则都可以,他的实现类ReentrantLock可通过构造器的bool参数传true构造成公平锁FairSync,传false则构造成非公平锁NonfairSync(默认)
- Lock底层是CAS
乐观锁,依赖AbstractQueuedSynchronizer(AQS)类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作,synchronized底层是使用指令码的方式来控制锁
3.1 死锁
- 产生原因多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止(例如线程A持有资源2,等待资源1,线程B持有资源1,等待资源2),死锁需满足互斥、请求和保持、不可剥夺、环路等待四个条件
- 互斥:该资源任意一个时刻只由一个线程占用
- 请求和保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
- 环路等待:线程之间形成一种头尾相接的循环等待资源关系
3.2 公平锁与非公平锁
- 公平锁:锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,其实就是先来先得。
- 非公平锁:按随机、就近原则分配锁的机制。
3.3 乐观锁与悲观锁
- 乐观锁
- 每次去拿数据都认为别人不会修改锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作,典型的乐观锁是CAS
- 悲观锁
- 每次去拿数据都认为别人会修改锁,常见的悲观锁synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁,如ReentrantLock。
4. wait和notify/notifyAll
- wait()、notify()以及notifyAll均是Object类的方法,所以所有对象均可以进行调用
- wait()有三个重载的方法,分别是wait(long timeout, int nanos)、wait(long timeout)以及wait()(本质上是调用wait(0)表示一直等待唤醒)
- notify方法只唤醒等待对象的一个线程,并且该线程开始执行。所以如果有多个线程在等待一个对象,这个方法只会唤醒其中的一个。线程的选择取决于线程管理的OS实现
- notifyAll方法唤醒等待对象的所有线程,但哪一个将首先处理取决于操作系统的实现
- 定义一个实体,将作为同一个对象进行加锁和唤醒
public class Message {
private String name;
public Message(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class WaitThread extends Thread {
private final Message message;
public WaitThread(String name, Message message) {
super(name);
this.message = message;
}
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
synchronized (message) {
try {
System.out.println(threadName + "使用wait()在等待");
message.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + "被notify()通知唤醒,等待完毕");
}
}
}
- 定义一个等待3S自动释放的线程,当然如果有notify()唤醒该线程,他也会提前释放
public class WaitTimeThread extends Thread {
private final Message message;
public WaitTimeThread(String name, Message message) {
super(name);
this.message = message;
}
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
synchronized (message) {
try {
System.out.println(threadName + "使用wait(3000)在等待3s");
message.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " 3s结束自动唤醒,等待完毕");
}
}
}
public class NotifyThread extends Thread {
private final Message message;
public NotifyThread(String name, Message message) {
super(name);
this.message = message;
}
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
synchronized (message) {
System.out.println(threadName + "使用notify()唤醒线程");
message.notify();
}
}
}
public class WaitNotifyDemo {
public static void main(String[] args) {
Message msg = new Message("消息");
WaitThread waitThread1 = new WaitThread("等待线程A", msg);
waitThread1.start();
WaitTimeThread waitTimeThread = new WaitTimeThread("等待线程B", msg);
waitTimeThread.start();
NotifyThread notifyThread = new NotifyThread("唤醒线程", msg);
notifyThread.start();
}
}
执行结果
等待线程A使用wait()在等待
等待线程B使用wait(3000)在等待3s
唤醒线程使用notify()唤醒线程
等待线程A被notify()通知唤醒,等待完毕
等待线程B 3s结束自动唤醒,等待完毕
4.1 wait和sleep的比较
- 具体可查看:baijiahao.baidu.com/s?id=164742…
- sleep是线程Thread类中的方法,但是wait是Object中的方法
- sleep方法不会释放锁,但是wait会释放,而且会加入到等待队列中
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
- sleep不需要被唤醒(休眠之后退出阻塞),但是wait是需要notify()或者notifyAll()去唤醒的,除了wait(long timeout)这种形式