一、卖票示例
需求:四个窗口,同时售卖100张票,票号为1-100
1、没有多线程时的卖票代码
class Ticket {
//100张票
private int num = 100;
public void sale() {
//无限循环,没有写break
while (true) {
if (num > 0) {
num--;
}
}
}
}
2、四个窗口同时卖票,要用多线程。尝试通过继承Thread类创建多线程
class Ticket extends Thread {
//100张票
private int num = 100;
public void sale() {
//无限循环,没有写break
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
sale();
}
}
public class Test {
public static void main(String[] args) {
//创建四个线程
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
问题:
(1)每个线程都有自己的num,一共卖出了400张票
(2)会出现 num=0 或 num=负数 的票。原因:num=1时,线程0已经做完 if(num > 0) 的判断,还没执行num--,此时线程切换到了线程1。线程1执行run()方法,继续判断 if(num > 0),符合,(此处可能没执行num--,线程再次进行了切换)打印输出,接着执行num--,此时num=0。...... 等到下次执行权再切回到线程0时,线程0不需要再做判断,直接输出,然后做num--操作。此时,线程0输出的num就可能是0或者负数 -- 具体参见第二点:线程安全
思考:
(1)将num改成静态static的?
静态实现数据共享,可以解决只卖一组票的问题
但这样一来,Ticket和num就没有关系了(num被所有对象共享)。而且如果现在有两组100张票,2个窗口负责卖前100张票,另外2个窗口负责卖后100张票,此时,使用static是4个线程卖100张票,不能解决问题
(2)创建一个Ticket线程,开启四次?
class Ticket extends Thread {
//100张票
private int num = 100;
public void sale() {
//无限循环,没有写break
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
sale();
}
}
public class Test {
public static void main(String[] args) {
//创建一个线程
Ticket t1 = new Ticket();
//开启四次
t1.start();
t1.start(); //报错。Exception in thread "main" java.lang.IllegalThreadStateException
t1.start();
t1.start();
}
}
问题:此种做法是错误的,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动
说明:此段代码有两个线程:主线程和t0线程。主线程调用第一个 t1.start(); 开启了一个线程t0,t0开始执行run()方法中的内容。当主线程在调用第二个 t1.start(); 时抛出异常,主线程立即结束。而t0线程会继续执行直至结束(在run()方法中发生异常才是t0线程的异常)
3、不能使用单例。用单例,内存中就只能有一个票对象,但实际是可以有多个票对象的。eg:每两个窗口卖一组100张的票
4、创建多线程,不能通过继承Thread类创建,那就只能通过实现Runnable接口创建。封装线程任务+封装资源
class Ticket implements Runnable {
//封装资源 -- 票的数据
private int num = 100;
public void sale() {
//无限循环,没有写break
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
sale();
}
}
public class Test {
public static void main(String[] args) {
//创建一个线程任务对象,内存中只有一个Ticket对象
Ticket t = new Ticket();
//创建线程。线程一初始化得有任务,向其构造函数中传参
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
说明:内存中只有一个Ticket对象,四个线程运算的num都是Ticket对象的num。因为把票Ticket传给了每个线程
5、如果想让两种票一起卖,两个窗口卖普通票,另外两个窗口卖动车票
public static void main(String[] args) {
//卖几组票,就创建几个Ticket对象
Ticket t = new Ticket();
Ticket tt = new Ticket();
//将不同的Ticket对象传给不同的线程,线程就有不同的资源
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(tt);
Thread t4 = new Thread(tt);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
二、线程安全
1、四个窗口,同时售卖100张票,用Runnable接口实现,存在的另一个问题:num可能为0或负数
class Ticket implements Runnable {
//封装资源 -- 票的数据
private int num = 100;
public void sale() throws InterruptedException {
//无限循环,没有写break
while (true) {
if (num > 0) {
//线程阻塞。调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
//run()方法是覆盖Runnable接口中的run()
//Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
try {
sale();
} catch (InterruptedException e) { //抛什么就catch什么
e.printStackTrace();
}
//不存在异常的代码要放在try/catch外
//......
}
}
public class Test {
public static void main(String[] args) {
//创建一个线程任务对象,内存中只有一个Ticket对象
Ticket t = new Ticket();
//创建线程。线程一初始化得有任务,向其构造函数中传参
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2、在写多线程时,必须要注意线程的安全问题
3、线程安全问题产生的原因/前提:
(1)多个线程在操作共享的数据
(2)操作共享数据的线程代码有多条
即 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生
(必须是多线程;得有共享数据;线程的代码当中是否有多条语句在操作同一个共享数据)
4、线程安全问题的解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
5、线程安全问题的解决方式:同步。包括同步代码块和同步函数
三、同步
1、同步代码块
(1)同步代码块的格式
synchronized (对象) {
//需要被同步的代码
//......
}
(2)synchronized(对象){...}:此处的对象就是同步锁/对象锁。放入一个对象(锁),是因为后期要对同步中的线程进行监视操作,而监视的方法都在锁上,所以,要加一个锁才行
注:每个线程执行前,都要先判断锁。一个线程得到CPU的执行权后,会先去判断锁。拿到锁后,开始执行。如果该线程被sleep(time),释放执行权的同时释放执行资格,但锁没有释放。另一个线程得到CPU的执行权后,也会先去判断锁,但获取不到锁,只能等待。直到被sleep(time)的线程重新获取CPU的执行权,并执行完其线程任务后,锁才会被释放。此时,其他线程才有机会获取锁并执行对应的线程任务
(3)修改后的代码
class Ticket implements Runnable {
//封装资源 -- 票的数据
// 可调大num,或减少sleep(time)时间,并让线程多运行几次,就可以在运行结果中看到所有线程
private int num = 300;
//用作 同步代码块 synchronized(对象){...} 中的对象,也可以自己再new其他对象
Object obj = new Object();
public void sale() throws InterruptedException {
//无限循环,没有写break
while (true) {
//同步代码块
synchronized (obj) {
if (num > 0) {
//调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
//run()方法是覆盖Runnable接口中的run()
//Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
try {
sale();
} catch (InterruptedException e) { //抛什么就catch什么
e.printStackTrace();
}
//不存在异常的代码要放在try/catch外
//......
}
}
public class Test {
public static void main(String[] args) {
//创建一个线程任务对象,内存中只有一个Ticket对象
Ticket t = new Ticket();
//创建线程。线程一初始化得有任务,向其构造函数中传参
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2、同步的好处与弊端
(1)好处:解决了线程的安全问题
(2)弊端:相对降低了效率,因为同步外的线程都会判断同步锁
3、同步的前提:
同步中,必须有多个线程,并使用同一个锁
错误示例
class Ticket implements Runnable {
//封装资源 -- 票的数据
// 可调大num,或减少sleep(time)时间,并让线程多运行几次,就可以在运行结果中看到所有线程
private int num = 300;
public void sale() throws InterruptedException {
//同步锁
//线程执行run()方法调用sale()时,都会创建一个Object锁对象,导致多个线程使用的不是同一个锁而出错
//error!!!
//要将创建锁对象的代码放在run()方法外
//synchronized(new Object()){...}的写法和此种错误原理相同,都是创建多个锁对象
Object obj = new Object();
//无限循环,没有写break
while (true) {
//同步代码块
synchronized (obj) {
if (num > 0) {
//调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
}
/**
* 封装线程任务
* 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
* 建议最好不要将sale()的方法名直接改为run()
*/
@Override
public void run() {
//run()方法是覆盖Runnable接口中的run()
//Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
try {
sale();
} catch (InterruptedException e) { //抛什么就catch什么
e.printStackTrace();
}
//不存在异常的代码要放在try/catch外
//......
}
}
public class Test {
public static void main(String[] args) {
//创建一个线程任务对象,内存中只有一个Ticket对象
Ticket t = new Ticket();
//创建线程。线程一初始化得有任务,向其构造函数中传参
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
错误原因:每个线程开启后,都有自己的run()方法。而每个run()中都 new Object(),意味着每个线程都有自己的锁。多个线程使用的不是同一个锁,出错。synchronized(new Object()){...}的写法和此种写法的错误原理相同(需要把创建锁对象的代码放在run()方法外)
注:在开发中,写了同步还没有解决问题时,需要考虑同步的前提:多个线程是否使用同一个锁
4、同步函数
需求:有两个储户,到同一家银行存钱,每人每次存100元,共存三次
class Bank {
//银行的小金库,不能对外暴露,用private
private int sum;
public void add(int num) throws InterruptedException {
sum += num;
//为了发现问题,此处调用sleep(time)方法切换CPU执行权
Thread.sleep(10);
System.out.println("sum=" + sum);
}
}
class Custom implements Runnable {
//两人去同一家银行,Bank是共享数据,new Bank()不能写在run()中
private Bank bank = new Bank();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
//此处的run()方法是覆盖Runnable接口的run(),而Runnable接口的run()没有声明异常
//所以,此处只能 try/catch
try {
bank.add(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
Custom custom = new Custom();
Thread t1 = new Thread(custom);
Thread t2 = new Thread(custom);
t1.start();
t2.start();
}
}
问:上述代码是否存在安全隐患?
分析:多线程代码是否存在安全隐患,得看在线程运行的代码中,是否有共享数据。run()方法中,bank是共享数据,但操作共享数据的代码只有一条 bank.add(100); ,看似没问题。但在run()方法中调用了add()方法,此时,add()方法也属于线程的代码。而add()方法中,sum是共享数据,且操作sum的代码不止一条。所以,上述代码存在安全隐患
解决:解决线程安全问题的方法:同步。包括:同步代码块和同步函数
(1)同步代码块
class Bank {
//银行的小金库,不能对外暴露,用private
private int sum;
//同步锁
private Object obj = new Object();
public void add(int num) throws InterruptedException {
//同步代码块
synchronized (obj) {
sum += num;
//为了发现问题,此处调用sleep(time)方法切换CPU执行权
Thread.sleep(10);
System.out.println("sum=" + sum);
}
}
}
(2)同步函数
class Bank {
//银行的小金库,不能对外暴露,用private
private int sum;
//使用同步函数解决线程安全问题时,不用自己创建同步锁
//同步函数的锁是this
// private Object obj = new Object();
//同步函数:将同步关键词synchronized作为函数的修饰符即可
public synchronized void add(int num) throws InterruptedException {
sum += num;
//为了发现问题,此处调用sleep(time)方法切换CPU执行权
Thread.sleep(10);
System.out.println("sum=" + sum);
/*
//同步代码块
synchronized (obj) {
sum += num;
//为了发现问题,此处调用sleep(time)方法切换CPU执行权
Thread.sleep(10);
System.out.println("sum=" + sum);
}
*/
}
}
5、卖票示例的同步函数写法
(1)使用同步代码块的写法
class Ticket implements Runnable {
//票
private int num = 100;
//同步锁
Object obj = new Object();
@Override
public void run() {
//无限循环,没有写break
while (true) {
//同步代码块
synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
(2)使用同步函数的写法
错误示例:
class Ticket implements Runnable {
//增加票数,为了更能凸显问题
private int num = 400;
//同步锁
// Object obj = new Object();
//同步函数的错误写法
@Override
public synchronized void run() {
//无限循环,没有写break
while (true) {
//同步代码块
// synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
// }
}
}
}
运行结果:线程0把400张票全都卖完了,票号是400-1,没有出现0号票。但这些票全是线程0卖的,其他三个线程没有参与
分析:run()方法是同步函数,线程0得到锁,又得到CPU执行权,进入run()运行。之后线程0执行sleep(time),释放执行权的同时释放执行资格,但没有释放锁。其他线程获取到CPU执行权,但是得不到锁,只能等待(线程0不释放锁,其他线程都得不到锁,不能执行run()方法)...... 等到线程0重新获取到CPU执行权后,把票卖光,依旧无法释放锁:while(true){...},无限循环
即 while(true){...}这句代码不需要被同步
解决:用另一个函数封装需要被同步的代码,再被run()调用即可
正确代码:
class Ticket implements Runnable {
//票
private int num = 100;
//同步锁
// private Object obj = new Object();
@Override
public void run() {
//无限循环,没有写break
while (true) {
//同步代码块
// synchronized (obj) {
//在run()中调用同步函数
show();
// }
}
}
/**
* 同步函数的正确写法:
* 定义一个同步函数,封装需要被同步的代码,再被run()方法调用即可
* 注:num是共享数据
*/
public synchronized void show() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
6、验证同步函数的锁
(1)同步函数的格式
public synchronized void xxx() { ... }
(2)验证同步函数的锁
思路:两个线程,一个在同步代码块中卖票,一个在同步函数中卖票。如果这两个同步用的是同一个锁,就不会出现安全隐患(出现重复的票、0或负号票)
做法:run()方法中既能运行同步代码块,又能调用到同步函数,需要做线程切换动作,要定义标志位flag
class Ticket implements Runnable {
//票
private int num = 400;
//同步锁
// private Object obj = new Object();
//标志位
boolean flag = true;
@Override
public void run() {
//同步函数的锁
//this所指向的地址与Ticket t所指向的地址是一致的
System.out.println("this:" + this); //this:test.Ticket@58ceff1
//如果flag=true,运行同步代码块
if (flag) {
//无限循环,没有写break
while (true) {
//同步代码块
// synchronized (obj) {
//为了验证同步函数持有的锁是this,将同步代码块的锁也改为this
synchronized (this) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "...同步代码块..." + num--);
}
}
}
} else {
//如果flag=false,运行同步函数
while (true) {
//其实是this.show(),this被省略
show();
}
}
}
/**
* 同步函数的正确写法:
* 定义一个同步函数,封装需要被同步的代码,再被run()方法调用即可
* 注:num是共享数据
*/
public synchronized void show() {
//需要被同步的代码
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "...同步函数..." + num--);
}
}
}
public class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
//Ticket t所指向的地址与this所指向的地址是一致的
System.out.println("t:" + t); //t:test.Ticket@58ceff1
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
//能处理的异常,用try/catch
try {
//此处调用sleep(time)是为了切换CPU的执行权去执行t1线程
//否则,开启t1线程后,主线程还持有CPU的执行权,它会继续向下,把下面的代码全部执行完,flag瞬间被改为false
//此时,t1和t2线程执行的都是flag=false的代码,不能体现切换标志位的操作(flag=true执行同步代码块,flag=false执行同步函数)
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改标志位
t.flag = false;
t2.start();
}
}
分析:同步函数具有同步性,但同步本身不带锁,应该是函数带锁。函数可以操作对象中的数据,是因为持有this引用,即每个函数都有自己所属的this。同步函数show()被run()方法调用,而run()方法属于 Ticket t(Ticket t 是调用run()方法的对象),所以,show()方法用this代表 Ticket t 对象
说明:run()方法是封装线程任务的,将线程任务所属对象t传给new Thread(t),所以,Ticket t 是调用run()方法的对象
总结:同步函数使用的锁是this
(3)同步函数和同步代码块的区别
同步函数的锁是固定的this,同步代码块的锁是任意的对象。建议使用同步代码块
注:同步函数可以作为同步代码块的简写形式。同步函数的锁唯一,就是this。只有同步代码块中用的锁是this时,才可以简写成同步函数
7、验证静态同步函数的锁
(1)静态同步函数的格式
public static synchronized void xxx() { ... }
(2)验证静态同步函数的锁
思路:静态同步函数的锁一定不是this(static方法中没有this)。静态函数随着类的加载而加载,而类进内存时,还没有通过new来创建对象。但java有个特点:字节码文件进内存先封装对象。所以,类一进内存,就有一个对象,即当前的.class文件所属的对象,而这个对象就是静态同步函数所使用的锁
注:所有对象建立,都有自己所属的字节码文件对象,可以用getClass()方法获取
class Ticket implements Runnable {
//票
private static int num = 400;
//同步锁
// private Object obj = new Object();
//标志位
boolean flag = true;
@Override
public void run() {
//静态同步函数的锁
System.out.println("this:" + this.getClass()); //this:class test.Ticket
//如果flag=true,运行同步代码块
if (flag) {
//无限循环,没有写break
while (true) {
//同步代码块
// synchronized (obj) {
//静态同步函数持有的锁是该函数所属的字节码文件对象。以下两种写法都可以
// synchronized (this.getClass()) {
synchronized (Ticket.class) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "...同步代码块..." + num--);
}
}
}
} else {
//如果flag=false,运行静态同步函数
while (true) {
show();
}
}
}
/**
* 静态同步函数
*/
public static synchronized void show() {
//需要被同步的代码
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//不存在异常的代码要放在 try/catch 外
System.out.println(Thread.currentThread().getName() + "...静态同步函数..." + num--);
}
}
}
public class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
//无论new多少个Ticket对象,t.getClass()都是同一个
System.out.println("t:" + t.getClass()); //t:class test.Ticket
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
//能处理的异常,就try/catch
try {
//此处调用sleep(time)是为了切换CPU的执行权去执行t1线程
//否则,开启t1线程后,主线程还持有CPU的执行权,它会继续向下,把下面的代码全部执行完,flag瞬间被改为false
//此时,t1和t2线程执行的都是flag=false的代码,不能体现切换标志位的操作(flag=true执行同步代码块,flag=false执行静态同步函数)
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改标志位
t.flag = false;
t2.start();
}
}
静态同步函数的锁的表示形式:
a)this.getClass()
b)(Ticket t = new Ticket();) t.getClass()
c)Ticket.class
说明:a)是非静态的,b)要先创建完对象,才能调用方法,c)直接使用 类名.class 属性
注:任何类,它们都有一个静态属性:类名.class 属性。这个属性可以直接获取该字节码文件的字节码文件对象
总结:静态同步函数使用的锁是:该函数所属的字节码文件对象。该对象可以通过 对象.getClass() 方法获取(非静态),也可以用 当前类名.class属性(静态)表示
(3)同步代码块 synchronized(对象){...} 中的同步锁,可以是:obj、this、类名.class等。对象是任意的,只要能保证多个线程用的是同一个锁即可
(4)如何选择使用哪个同步锁
a)如果同步函数和同步代码块同时出现,且需要共用同一个锁,同步代码块的锁用 this
b)如果静态同步函数和同步代码块同时出现,且需要共用同一个锁,同步代码块的锁用 当前类名.class