目录
wait 和 notify 的调用必须是同一个对象调用。谁让他等待了,谁才能让他唤醒。解铃还须系铃人。
线程同步问题
多个线程同时访问一个资源
原因:
多个线程的执行是抢占式的,当一个线程执行方法时,可能会被另一个线程抢占CPU,当前线程的操作不能完整的执行,导致数据出现问题。
public void transfer(int from,int to,int money){
//一个线程扣除一个账户一定金额,准备给另一个账户加钱
//被其他线程抢占CPU执行,其他线程执行完转账后,前面线程扣的钱还没有加
//统计钱总数就出现问题
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
解决方法:
让一个线程能完整的执行完任务后,另一个线程再执行
上锁机制:
1. 同步代码块
2. 同步方法
3. 同步锁
同步代码块
可以对一段代码上锁
语法:
synchronized(锁对象){
需要上锁的代码
}
锁对象:
任意的成员变量对象都可以作为锁,因为在Object类中定义了锁的方法。对象必须是成员对象
public void transfer(int from,int to,int money){
synchronized (lock) {
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
}
原理:
当一个线程进入同步块后,JVM启动一个Monitor(监视器)对同步块进行监控,如果另一个线程想进入这个同步块,监视器会拒绝,当前面线程执行完代码,释放锁后,监视器才会让其他线程进入。
锁类:
锁普通方法可以锁对象,锁静态方法要锁类
public static void transfer(int from,int to,int money){
synchronized (本类.class) {
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
}
同步方法
public synchronized 返回值 方法名(参数){
代码
}
面试题:同步块和同步方法的区别
1)锁的粒度不同
同步块粒度小,可以只锁一段代码
同步方法粒度大,锁的是整个方法
2)锁对象不同
同步块任意成员对象都可以作为锁
同步方法锁对象是this
同步锁
jdk1.5出现的,类似同步块,但性能更高,功能更强。
Lock接口
实现类:
ReadLock 读锁
WriteLock 写锁
ReadWriteLock 读写锁
ReentrantLock 重入锁
用法
1)创建ReentrantLock 的成员变量
Lock必须是成员变量才行,局部变量还是其他的都不行
2)上锁 ,调用lock()
3)释放锁,调用unlock(),注意unlock必须执行,否则就会死锁
锁.lock();
try{
需要上锁代码
}finally{
锁.unlock();
}
性能:
同步锁 > 同步代码块 > 同步方法
总结:
锁机制会提高多线程情况下数据安全性,但是会降低程序的性能
线程的死锁
可能出现的原因:
1)使用同步锁之后没有手动释放锁
2)两个线程互相合并对方
3)两个线程都需要对方的锁,又都持有对方的锁
两个方法、两个锁、两个同步块相互嵌套、两个线程
锁1
锁2
方法1(){
sync(锁1){
....
sync(锁2){
}
}
}
方法2(){
sync(锁2){
....
sync(锁1){
}
}
}
线程的等待和通知
Object类的方法:
等待:
void wait() 让当前线程进入等待状态
void wait(long time) 让当前线程等待一定时间,时间过后线程恢复执行
通知:
void notify() 通知等待的其中一个线程,让其恢复执行
void notifyAll() 通知所有等待的线程
注意:
1)上面的四个方法必须是锁对象,如果不是就会抛出异常IllegalMonitorStateException。
2)通知线程的锁对象,必须是让线程进行等待的锁对象
机制:
线程执行到同步块或同步方法时,监视器会对线程进行监视,当调用wait或notify时,JVM会检查调用方法的对象是否是同步块或方法的锁对象,如果不是就抛出异常。
面试题:wait和sleep的区别
1)wait时线程会释放锁,sleep时线程不会释放锁
2)调用方式:wait可以由任意锁对象,sleep是通过Thread类调用
3)wait可以不设置时间,sleep必须设置睡眠时间
4)wait可以被通知,sleep必须等睡眠时间结束
生产者消费者设计模式
不是GOF23模式之一,是和线程相关的模式
在线程世界中生产和消费的是数据,有些线程用于生产数据就是生产者,有些线程用于使用数据就是消费者,生产者生产数据的速度和消费者消费数据的速度会出现不一致,生产者的速度过快会浪费系统资源,消费者速度过快会浪费系统资源和用户时间,生产者和消费者模式主要用于协调生产者线程和消费者线程的速度。
几个重要点:
1)缓冲区
2)线程
3)等待
4)通知
思路:
首先会建立缓冲区用于存放数据,生产者线程生产的数据会放入缓冲区,如果缓冲区满了,就让生产者线程等待,通知所有消费者线程来消费,如果缓冲区空了,就让消费者线程等待,通知所有生产者来生产。
/**
* 包子类
* @author Administrator
*
*/
public class Baozi {
private int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "Baozi [id=" + id + "]";
}
}
---------------------------------------------------
/**
* 包子仓库
* @author Administrator
*
*/
public class BaoziStore {
//缓冲区上限
private static final int MAX_COUNT = 50;
//包子缓冲区
private List<Baozi> baozis = new ArrayList<>();
//做包子
public synchronized void makeBaozi(){
//如果缓冲区满了,就让生产者线程等待
if(baozis.size() == MAX_COUNT){
System.out.println("仓库满了,生产者等待");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果缓冲区没满,通知生产者继续做包子,以及消费者来吃包子
this.notifyAll();
}
//创建包子,保存到缓冲区
Baozi baozi = new Baozi(baozis.size() + 1);
System.out.println("生产者做了"+baozi);
baozis.add(baozi);
}
//拿包子
public synchronized void takeBaozi(){
//如果缓冲区空了,就让消费者线程等待
if(baozis.size() == 0){
System.out.println("仓库空了,消费者等待" + Thread.currentThread().getName());
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果缓冲区没满,通知生产者继续做包子,以及消费者来吃包子
this.notifyAll();
}
//从缓冲区删除一个包子
if(baozis.size() > 0){
Baozi baozi = baozis.remove(baozis.size() - 1);
System.out.println(Thread.currentThread().getName()+"吃了"+baozi);
}
}
}
----------------------------------------------------------
public class BaoziTest {
public static void main(String[] args) {
//创建包子仓库
BaoziStore store = new BaoziStore();
//创建生产者线程生产100个包子
new Thread(()->{
for(int i = 0;i < 100;i++){
store.makeBaozi();
}
}).start();
//创建100个消费者线程吃1个包子
for(int i = 0;i < 100;i++){
new Thread(()->{
store.takeBaozi();
}).start();
}
}
}
线程池
线程是一种系统级的资源,创建和销毁是需要消耗系统资源的,如果大量使用线程,推荐使用线程池,线程池能够对线程进行回收利用,从而节约系统资源。
一般的线程执行完后,就进入死亡状态,被回收;在线程池中的线程执行完任务后,会进入等待状态,直到有新的任务,再通知线程执行。
API:
Executor 接口
execute(Runnable runnable) 在线程池中获得线程执行任务
ExecutorService 接口 继承Executor
shutdown() 停止,等待所有线程执行完
shutdownNow() 停止,强制停止所有线程
Executors线程池工具类
帮助创建各种线程池
newCachedThreadPool() 创建长度不限的线程池
newFixedThreadPool(int size)创建长度固定的线程池
size是线程的最大数量
前两种的区别:
长度不限的线程池,可能在并发量大的情况下创建很多线程,对系统的压力比较大;长度有限的线程池,可以控制并发量
size 线程数量应该是CPU的核心数 * N (N大于等于1取决于任务执行时间和并发量)
内核数:Runtime.getRuntime().availableProcessors()
newSingleThreadExecutor() 创建单一线程池
作业:
1)使用继承Thread、Runnable和各种线程池启动线程
2)编写线程安全的单例模式
3)使用生产者消费者模拟12306网站,4个服务器每个提供50张票,100个消费者每人买两张票
4)自学:使用阻塞队列实现生产者消费者模式