synchronized
synchronized 用在方法签名上,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行方法该方法,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。否则synchronized关键字将失去意义。
synchronized 用在代码块的使用方式:synchronized(obj) { //todo code here }
当线程运行到该代码块内,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会形成互斥!特别的,当obj == this时,表示当前调用该方法的实例对象。
使用synchronized 代码块相比方法有两点优势:
-
可以只对需要同步的使用
-
与wait()/notify()/nitifyAll()一起使用时,比较方便
wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。
而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!
notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
notifyAll()则是唤醒所有等待的线程。
生产者消费者模型:两个线程依次打印"A""B",总共打印10次。
public class Consumer implements Runnable {
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (CPTest.obj) {
System.out.print("B");
count--;
CPTest.obj.notify(); // 主动释放对象锁
try {
CPTest.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Produce implements Runnable {
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (CPTest.obj) {
System.out.print("A");
count--;
CPTest.obj.notify();
try {
CPTest.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CPTest {
public static final Object obj = new Object();
public static void main(String[] args) {
new Thread(new Produce()).start();
new Thread(new Consumer()).start();
}
}
Lock
除了wait()和notify()协作完成线程同步之外,使用Lock也可以完成同样的目的。
ReentrantLock 与synchronized有相同的并发性和内存语义,synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock()。
public class Consumer implements Runnable {
private Lock lock;
public Consumer(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
try {
lock.lock();
count--;
System.out.print("B");
} finally {
lock.unlock(); //主动释放锁
try {
Thread.sleep(91L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Producer implements Runnable {
private Lock lock;
public Producer(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
try {
lock.lock();
count--;
System.out.print("A");
} finally {
lock.unlock();
try {
Thread.sleep(90L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CPTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Consumer consumer = new Consumer(lock);
Producer producer = new Producer(lock);
new Thread(producer).start();
new Thread(consumer).start();
}
}
使用建议:
在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。
synchronized 关键字和 volatile 关键字的区别
-
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。 -
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。 -
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。
以i++为例,这其实是一个复合操作,包括三步骤:
- 读取i的值。
- 对i加1。
- 将i的值写回内存。 volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 线程池、数据库连接池、Http 连接池等等
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
通过 ThreadPoolExecutor 的方式创建线程池
模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。
AQS
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。