1. 线程的同步机制
要解决的问题:多个线程同时操作同一个资源(并发)
同时抢一张票,为什么会变成负数???
每个线程都在自己的工作内存进行交互,内存交互不当造成数据不一致,当2个线程同时看见剩一张票,都将这个1拿到自己的工作内存中,进行购买。第一买完就是0,第二个买完就是-1。
为什么会同时抢到一张票???
没有排队造成几个线程同时拿到同一张票
形成条件:队列+锁,解决线程不安全的问题,称为线程同步synchronized
线程同步的问题:
1. 损失性能问题
2. 优先级低的先拿到锁,会引起性能倒置的问题
2. 同步方法及同步块
同步方法:public synchronized void method(int args){}
synchronized方法控制对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
同步块:synchronized(Obj){}
不需要锁整个方法,浪费资源,只锁变动的资源就可以了
Obj称为同步监视器:
Obj可以是任何对象,推荐使用共享资源作为同步监视器
同步方法中无需只指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
同步监视器的执行过程:
1. 第一个线程访问,锁定同步监视器,执行其中代码
2. 第二个线程访问,发现同步监视器被锁定,无法访问
3. 第一个线程访问完毕,解锁同步监视器
4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
注:
在使用多线程的时候,ArrayList是线程不安全的,用JUC下的包CopyOnWriteArrayList是线程安全的
3. 死锁
多个线程互相抱着对方的资源,形成僵持的现象
产生死锁:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/e6a0c04011180b619524864c9a82eb08.png)
解决死锁:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/1ec62b515dbac53e8f0352f568d2a7f8.png)
产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不变
- 不剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
4. Lock
跟synchonized作用一样,Lock就是通过显示定义同步锁对象来实现同步。在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
建议lock()写入try中,unlock()写入finally中。
synchronized与lock的对比:
- Lock是显示锁,需要手动开启和关闭,synchronized是隐式锁,出了作用域自动关闭
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁JVM将花费更小的时间调度线程,性能更好,并有更好的扩展性
- 优先使用顺序:
Lock -> 同步代码块 -> 同步方法
5. 线程协作-生产者消费者
这是一个线程同步问题, 生产者和消费者共享同一资源,并且生产者和消费者之间相互依赖,互为条件。
synchronized只能实现线程的同步,不能实现线程的通信。java提供几种方法解决线程之间的通信问题。
wait() 线程等待,直到线程通知,与sleep不同,会释放锁
wait(long timeout) 指定等待毫秒数
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上,所有调用wait方法线程,有限级别高的线程优先调度
以上方法只能在同步方法或者同步代码块中使用
5.1 管程法
将数据放在缓冲区
public class TestPC(
public static void main(String[] args){
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
Productor(SynContainer container){
this.container = container;
}
@Override
public void run(){
for(int i = 0; i < 100; i++){
container.push(new Chicken(i));
System.out.println("生产出第" + i + "只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
Consumer (SynContainer container){
this.container = container;
}
@Override
public void run(){
for(int i = 0; i < 100; i++){
System.out.println("消费了第" + container.pop().id + "只鸡");
}
}
}
//产品
class Chichen{
int id ;
Chichen(int id){
this.id = id;
}
}
//缓冲区
class SynContainer{
//设置容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了就暂停生产,等待消费者消费
if(count == chickens.length){
//这里有个try...catch,此处省略
this.wait();
}
//如果容器没满,生产者丢入产品
chickens[count] = chicken;
count++;
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//如果容器空了,等待生产者生产
if(count == 0){
this.wait();
}
//如果容器没空,消费产品
count--;
Chicken chicken = chickens[count];
//通知生产者生产
this.notifyAll();
}
}
5.2 信号灯法
通过标志位
代码中没有container的类,在产品的类中通过flag标志位判断什么时候wait什么时候notify
6. 线程池
思路:提前创建好多个线程,放入池子中,使用时直接获取,使用完放回。不用频繁的创建和销毁,提升性能。
好处:
提高响应速度,降低资源消耗
便于线程管理:核心池大小、最大线程数、线程没有任务时最多保持多长时间会终止
Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
ExecutorService 真正的线程池接口,常见子类ThreadPoolExecutor
void execute(Runnable command) 执行任务/命令,一般用来执行Runnable
Future submit(Callable task) 执行任务,一般用来执行Callable
void shutdown() 关闭线程池