作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
上期文章:多线程&JUC:线程的生命周期与安全问题
订阅专栏:多线程&JUC
希望文章对你们有所帮助
上一部分讲解了面试可能会问的线程的生命周期,并且演示了超卖问题来讲解多线程并发的安全问题,超卖问题这是一个经典例子,这里会解释一下解决的方法。
如果是想要解决集群下的线程安全问题,可以学习我在做Redis项目的时候的解决方法:
Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)
Redis:原理速成+项目实战——Redis实战9(秒杀优化)
感兴趣还可以看看如何使用异步下单来实现秒杀,这些实现其实都跟线程的思想都是相关的:
Redis:原理速成+项目实战——Redis实战10(Redis消息队列实现异步秒杀)
在上一篇文章的demo中,发现了线程安全问题,不仅同样的票出现了多次,还出现了超出范围的票。
可以看关键的两条代码:
ticket++;
System.out.println("在卖第" + ticket + "张票");
由于CPU执行代码的过程中,其执行权随时会被其他的线程抢走,所以这样的代码会出现一些问题:假设线程1已经执行完了ticket++
,还没来得及执行输出语句,线程2就参与了ticket++
的操作,这时候就有可能出现输出同一张票的情况。而当ticket=99的时候,若三个线程同时进入if条件,这时候就很可能出现ticket>100的情况,也就是超卖现象。
由于上述的问题,我们可以想到一个方案,就是当有线程抢夺到CPU执行权的时候,将执行的代码全部锁起来,使得其他线程无法执行代码,这样就不会发生上面的问题。
将其锁起来,需要使用到关键字synchronized
,格式如下:
synchronized(锁){
//操作共享数据的代码
}
因此接下来需要编写一下这个锁对象,需要满足以下特点:
1、锁默认打开,有一个线程进去了,锁自动关闭
2、里面的代码全部执行完毕,线程出来,锁自动打开
这个锁对象,只要能保证是唯一的,那么锁对象可以非常随意的去定义,这种方式就叫作同步代码块
,代码如下:
public class MyThread extends Thread {
static int ticket = 0;
//锁对象,一定要是唯一的,可以加static关键字
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
if(ticket < 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!");
}else{
break;
}
}
}
}
}
1、synchronize这个关键字,我写到了while里面,这部分是不能写到while外面的,不然的话,就会出现100张票只被1个窗口卖光,显然是不符合现实场景的。
2、锁对象必须要是唯一的,学操作系统的时候就学过临界资源,意思其实是一样的,因此可以发现上面代码中的锁对象obj是加上了static
关键字的。除了这种方法,其实更常见的方法是使用字节码对象
,因为字节码对象是唯一的,因此上述的锁可以写成:
synchronized (MyThread.class){
//...
}
如果我们要将一个方法里面的所有方法都锁起来,那就没必要锁代码片段,而是锁住整个方法了。
同步方法,就是把synchronized关键字加到方法上,格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {…}
同步方法有2个特点:
1、同步方法会锁住方法里面所有的代码
2、锁对象不能自己指定,而是java自己默认规定好的:
(1)非静态方法:this
(2)静态方法:当前类的字节码文件对象
3窗口卖100张票的问题也可以用同步方法来解决:
1、定义MyRunnable类,实现Runnable接口,而里面的ticket没必要再设置成静态的了,因为主程序中只会将MyRunnable类创建一次,作为一个参数传递到线程中。
public class MyRunnable implements Runnable{
int ticket = 0;
@Override
public void run() {
while (true){
if (method()) break;
}
}
//这里的锁对象为this,由于主程序中MyRunnable对象是唯一的,因此锁对象也是唯一的
private synchronized boolean method() {
if (ticket == 100){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
2、编写测试类代码:
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
我自己在使用字符串拼接的时候,很喜欢使用StringBuilder,而且也阅读过底层的源码,这是一种效率很高的方式,而如果打开api帮助文档,可以发现StringBuffer和StringBuilder几乎是一样的方法,完成的功能也是一样的,而java为什么要设置两个功能一样的类呢?
打开StringBuffer的底层源码,我们可以发现StringBuffer的所有方法都有带有synchronized
关键字,即每个方法都是同步方法:
而StringBuilder底层是没有这个关键字的,因此StringBuffer在多线程下是安全的,满足了线程同步的特点。
当我们实现需求的时候,如果是多线程的,就使用StringBuffer,否则就使用StringBuilder(StringBuffer是会损耗一些时间的)。
synchronized的锁对象是自动开关的,而Lock锁可以时间手动的开关锁,Lock的实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock中提供了获得锁和释放锁的方法:
void lock():获得锁
void unlock():释放锁
Lock是接口,不能直接实例化,所以要采用它的实现类ReentrantLock来实例化,直接使用它的空参构造即可。
使用Lock锁,则MyRunnable类(若是MyThread类,由于会被创建多次,锁又必须要唯一,那么Lock前面就得加上static
)应修改为:
public class MyRunnable implements Runnable{
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if (ticket == 100){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
lock.unlock();
}
}
}
这样写,线程安全问题确实不会发生,但是程序却没办法终止。
因为当我们的票数为100时,我们直接break跳出循环了,所以没有执行释放锁的语句,其他的线程就在while循环里面一直等待锁的释放,这显然不合理,一种简单的解决方法是在if里面继续加一条释放锁的语句:
if(ticket == 100){
lock.unlock();
break;
}
这样的方式固然可行,但是这写了两次unlock不是很符合规范。
更规范的方式是使用try...catch...finally
,无论如何,程序最终都必须要执行finally里面的语句,上述代码最终可以改写为:
public class MyRunnable implements Runnable{
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
if (ticket == 100){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
} catch (RuntimeException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}