多线程中关于线程安全的案例到解决方案到StringBuffer与StringBuilder的初辨
有这样一个需求
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
*/
针对于多线程的练习,我们可以考虑到卖票之间的窗口互相独立,就是多线程问题,考虑到如果说只是简单实现多线程,创建线程对象,然后启动三个线程,可能会有以下问题:
- 如果是第一种实现多线程的方式,也就是继承Thread类,将成员方法ticket没有设置成静态的,在创建三个线程对象后,会出现最后卖了三百张票的情况。
- 仅仅调整1出现的问题,不能满足需求,此时我们会发现可能出现几个窗口再卖同一张票售出,意味着一个座位将坐多个人。
- 我们还会发现当运行到100张票之后有超出的票,使得多出的人没有办法坐的尴尬情况。
这实际上是多线程中的线程安全,如果一个线程进入循环中,睡眠的情况下稍作停留,同时会有其他线程进入。那么就需要我们同步代码块,同步代码块相当于只允许一个线程进去,其他线程抢占到了权也不能立马进入,要等里面的线程执行完毕后才能进入。重点解决如何书写同步代码块的方法。
synchronized(){}
我们使用synchronized关键字,将需要循环内部的代码块放入,使用synchronized需要注意两个细节
- synchronized不能写在循环的外面,如果写在循环的外面,就表示当前的那个进入的线程,全部执行完循环之后才能出来,其他线程没有进入循环的机会。
- synchronized()括号中的参数表示锁对象,锁对象可以随意起,是Object类也可以。但是锁对象一定要是唯一的,如果用Object类来写,那么创建对象前要加上static表示成员共享。
static Object obj = new Object(); //MyThread不管创建多少对象,这里的obj都是共享的,都是同一个
这里括号中的参数通常使用当前类的字节码文件对象,我当前类叫做MyThread,那么就是MyThread.class,因为它唯一,拿过来当作锁对象。这样就可以初步解决问题了。
初步解决问题的源码:
MyThread类:
public class MyThread extends Thread{
static int ticket = 0;//0~99
//表示这个类所有的对象都共享ticket这个数据
/*
细节1:
synchronized不能写在循环的外面
细节2:
synchronized锁对象一定要是唯一的
一般我们会写当前类的字节码文件对象 MyThread.class 唯一,拿过来当作锁对象
*/
//锁对象,一定要是唯一的
//static Object obj = new Object(); //MyThread不管创建多少对象,这里的obj都是共享的,都是同一个
@Override
public void run() {
//卖票的代码逻辑
while (true){
//已经把操作共享的数据锁起来了,不管有的多少条线程,这里面的代码都是轮流执行
synchronized (MyThread.class){ //锁对象,解决线程安全的问题
//this 表示当前线程本身,放到锁对象就是不一样的,又会有不一样和重复的
//一般我们会写当前类的字节码文件对象 MyThread.class 唯一,拿过来当作锁对象
//同步代码块
if(ticket < 100){
//让当前线程停顿一会
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();//父类中run方法没有抛,子类就不能抛
//只能自己Try
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "票!!!" );
}else {
break;
}
}
}
}
}
ThreadDemo类代码:
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
*/
//三个窗口互相对立,三个线程
//三个窗口执行同样的逻辑,所以把ticket写在MyThread中
//创建三个线程执行相同的代码
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
//现在的问题就是窗口1,2,3总共卖了300张票
//我们要在ticket的前面加一个static
//这样三个窗口总共卖100张票
//相同的票出现了多次,还有超出范围的票
//解决,同步代码块
}
}
先理一下同步代码块的书写步骤:
- 循环
- 书写同步代码块
- 判断共享数据是否到了末尾,如果到了末尾
- 判断共享数据是否到了末尾,如果没有到末尾
接下来我们需要重新改写为同步方法,
书写同步方法的步骤如下:
- 先写同步代码块
- 选中要同步方法的方法体,在IDEA中按住ctrl+alt+M快速创建方法,修改名称...
- 然后删去原来方法外层synchronized(){},然后在修饰符private(IDEA默认帮我们创建的方法修饰符)后加上synchronized关键字。
这样就可以很好的改写为同步方法了,源码我会放到最底下。
由线程安全我想到StringBuilder,和StringBuffer,它们有什么区别呢?
首先查阅JavaAPI帮助文档
我们会发现它们的成员方法高度一致,在翻阅文档的时候,我看到了一句话
指出StringBuilder是有线程安全问题的,那么为什么呢?我们可以翻看源码来解释。
对比两个类的成员方法,我们会发现左边StringBuffer方法有synchronized关键字修饰作为同步方法,而右边没有。
启示:
如果说将来我们只需要单线程,那么我们可以使用StringBuilder,而涉及到多线程问题,我们需要使用StringBuffer。
同步代码块的源码:
MyRunnable类:
public class MyRunnable implements Runnable {
//这里没必要加static
int ticket = 0;
//第一种方式有可能创建多个对象,我想让它们共享一个成员变量的值,所以加static
//第二种方式实现多线程,MyRunnable作为一个参数让线程执行,我只会创建一次,ticket没有必要加static
@Override
public void run() {
//1.循环
while (true) {
//当前类的字节码文件作为锁对象
//2.同步代码块(同步方法)
//同步方法的书写步骤:
//①按住ctrl+ alt + M抽取成一个方法,修改名字...
//②然后删去synchronized(){}然后在方法修饰符private后面加上synchronized
if (method()) break;
}
}
//this,此时方法非静态,锁对象为this ,对应ThreadDemo中mr唯一,锁对象唯一
private synchronized boolean method() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
return true;
} else {
//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
ThreadDemo类:
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
利用同步方法完成
哪些代码写在同步当中?
技巧:同步代码块
抽取成一个方法
*/
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();
}
}
我书写这篇文章的初衷就是总结学习的进度,遗忘之际可以拿出来翻看,如有不对的地方还望指正,多多海涵。