回顾上文:我们知道
- 通过继承Thread类可以实现任务和线程相结合。
- 在A线程中调用B线程的
join,则A线程会被阻塞,直到B线程执行完毕后A线程才能执行,实现了某种意义上的顺序执行。- 通过线程对象调用
interrupt的方式,可以将该线程设置一个中断标志,程序内部可以使用isInterrupted去判断是否有这个终端标志,如果被中断了则调用isInterrupted会返回true。- 通过实现
Thread.UncaughtExceptionHandler定义异常捕获器,如何在线程中设置异常捕获器,即可捕获在线程中抛出的异常。
本文内容:
- 使用线程共享资源时出现的问题。
- 解决共享资源竞争
- 学习两种锁
synchronized和lock
模拟一个抢票的程序
该Demo程序,定义了一个属性资源为5,它有一个方法是sub方法,就是一个递减方法。
Demo中还定义了一个静态类,作为线程,这个线程就是不断的去执行int res = demo.sub();就是去抢夺资源。
在主线程中,我们使用线程池去驱动线程,用循环开启线程,相当于开启了100个线程去抢夺资源。
public class Demo {
private int num = 5;
public int sub() {
--num;
return num;
}
static class Thread1 extends Thread{
private Demo demo;
public Thread1(Demo demo) {
this.demo = demo;
}
@Override
public void run() {
while (true) {
try {
TimeUnit.MILLISECONDS.sleep(50);
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
int res = demo.sub();
if (res >= 0) {
System.out.println(currentThread().getName() + "执行了操作-1 还有:" + res);
} else {
return;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executorService.execute(new Thread1 (demo));
}
executorService.shutdown();
}
}
我们观察输出结果:
pool-1-thread-47执行了操作-1 还有:0
pool-1-thread-41执行了操作-1 还有:1
pool-1-thread-44执行了操作-1 还有:3
pool-1-thread-50执行了操作-1 还有:3
pool-1-thread-48执行了操作-1 还有:2
pool-1-thread-42执行了操作-1 还有:4
发现有两个操作,44线程和50线程在操作时,操作了相同的num资源,这并不是我们想看到的,但这是线程共享资源造成的原因,我们必须要解决这个问题。
解决共享资源竞争--锁
synchronized
锁对象建议使用
final进行修饰
概念
我们需要某种方式来防止两个任务访问相同的资源。该方法就是当资源被一个任务使用时,在其上加锁,使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种相互排斥的效果,所以这种机制常称为互斥量。
synchronized方法声明
标记在方法上或用在方法体中。
Java提供synchronized关键字,用来防止资源冲突。synchronized是一个加锁的动作,而在一个类中锁有两种:
- 对象都自动含有单一的锁(监视器)。
- 类本身也有个锁,可以对
static数据进行并发的控制。
上面这两种其实意思是,你可以用synchronized去防止对static数据进行并发访问,因为可以防止对非static数据进行并发访问。
声明synchronized方法
synchronized void f()
synchronized void g()
synchronized static void f()
如果一个类对象中有两个synchronized方法,则要明白锁只有一个,如果A线程拿到了锁,操作了f(),其他线程只能等待A线程释放了锁才可以调用g()。
另外锁还有一个计数机制,就是一个线程任务可以多次获得对象的锁(同一把锁),简单点说,A线程拿到了锁操作f(),它现在获得了锁,能被允许继续获得多个锁。加锁时执行+1的操作,释放锁的时候-1,当计数变为0时,完全释放锁。
注意:锁是保护资源的,所以要将资源的域设置为
private,这样可以设置一个统一的public方法去操作资源,而这个public方法可以使用synchronized去保护操作资源时发生的资源竞争的问题。
什么时候需要用synchronized
- 涉及到资源的操作
- 如果这个资源(变量)接下来将被不同的线程读取
- 或者操作一个已经被其他线程操作过的资源(变量)
这就需要用到加锁的操作了。
上述用synchronized解决资源抢夺的问题
public synchronized int sub() {
--num;
return num;
}
Lock对象
更加灵活的锁
相较于对象锁、类内建锁。我们可以显式的声明一把锁,这把锁的生命周期由我们手动控制:
- 显式地创建
- 锁定
- 释放
改写用lock加锁
private Lock lock = new ReentrantLock(); // 声明锁
private int num = 5;
public int sub() {
lock.lock(); // 加锁
try {
--num;
return num;
}finally {
lock.unlock(); // 释放锁
}
}
如果光是这样,还不足以有趣,我们再写个Demo,感受一下
本示例来自《Java编程思想》,我在此基础上做了一些修改
这个Demo主要是想介绍一下ReentrantLock对象
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
// 正常锁操作 和 解锁操作
public void untimed(){
boolean captured = lock.tryLock();
try {
System.out.println("tryLock():" + captured);
}finally {
if (captured){
lock.unlock();
}
}
}
// 等待锁操作和解锁操作,如果没有拿到锁就反复等待,直到拿到锁
public void timed(){
boolean captured = false;
try {
captured = lock.tryLock(2,TimeUnit.SECONDS); // 两秒去锁一次
while (!captured){
captured = lock.tryLock(2,TimeUnit.SECONDS); // 反复去锁
System.out.println("正在尝试");
}
System.out.println("拿到锁");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final AttemptLocking al = new AttemptLocking();
// al.untimed();
// al.timed();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
al.lock.lock(); //忘记解锁
System.out.println("acquired");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
al.lock.unlock(); // 尝试解锁,如果把这个 try...catch...finally去掉,可以再感受一下
}
}
});
thread.setDaemon(true);
thread.start();
Thread.yield();
al.untimed();
al.timed();
}
}