Java 多线程 —— 同步代码块,联通 java 开发面试
}
}
package test.MyThread.ticketDemo;
public class ticketDemo1 {
public static void main(String[] args) {
RunnableThread r1 = new RunnableThread();
Thread t1 = new Thread(r1,"窗口一");
Thread t2 = new Thread(r1,"窗口二");
Thread t3 = new Thread(r1,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
但是结果和我们想象中的不一样,三个窗口卖出了同样的票
这是因为,CPU 的操作具有原子性,单独执行一条指令或者说语句,在执行完毕前不会被中断。
三个线程被启动后,都会处于就绪状态,然后开始抢夺 CPU 执行语句。
语句一:Thread.sleep(100);
语句二: System.out.println(Thread.currentThread().getName()+“正在出售第 “+ticket+” 张票”);
语句三: ticket–;
我将程序中需要执行的三条主要语句列了出来
三条线程中,加入线程一先抢到了 CPU,这时就会开始执行语句,也就是至少会完成一条语句一,然后进入休眠。
注:如果语句一不是休眠语句,而是别的语句,那么线程一就可以继续往下执行,因为原子性,正在执行的语句不会被打断,所以只会在一条语句结束,下一条语句未开始时,被抢走 CPU 或者中断,导致线程退出运行状态,转为就绪或者阻塞状态。所以线程一可以一次性完成多条语句,也有可能刚完成一条语句就被抢走了 CPU。
接着,线程二,线程三也抢到了 CPU,也开始执行语句一,然后也进入休眠状态。之后线程一二三从休眠中醒来,开始争抢 CPU 完成语句二,但是三者都在完成语句三之前被抢走了 CPU,导致一直没有执行 ticket–语句,ticket 也就没有减少,因此三条线程一共打印三条输出语句,里面的 ticket 都是相同。
然后三条线程又开始争抢 CPU 来完成语句三,一个线程让 ticket 减一,三个线程减少三张票。完成语句三后,又开始新的循环,三个线程开始争抢 CPU 完成语句一。
因此,看到的结果会是,三条语句的 ticket 都相同,然后 ticket 突然减三,接着又输出三条 ticket 相同的输出语句。
那么,该如何解决这种情况呢?
这种延迟卖票的问题被称为线程安全问题,要发生线程安全问题需要满足三个条件(任何一共条件不满足都不会造成线程安全问题):
是否存在多线程环境
是否存在共享数据/共享变量
是否有多条语句操作着共享数据/共享变量
火车站延迟卖票问题满足这三个条件,因此造成了线程安全问题,而前两条都不可避免,那么就可以着手于破坏掉第三个条件,让线程安全问题不成立。
思路是将多条语句包装成一个同步代码块,当某个线程执行这个同步代码块的时候,就跟原子性一样,其他的线程不能抢占 CPU,只能等这个同步代码块执行完毕。
解决办法:
synchronized —— 自动锁
lock —— 手动锁
synchronized
synchronized(对象){
//可能会发生线程安全问题的代码
}
//这里的对象可以是任意对象,我们可以用 Object obj = new Object()里面的 obj 放入括号中
使用 synchronized 的条件:
必须有两个或两个以上的线程
同一时间只有一个线程能够执行同步代码块
多个线程想要同步时,必须共用同一把锁
synchronized(对象)括号里面的对象就是一把锁
使用 synchronized 的过程:
只有抢到锁的线程才可以执行同步代码块,其余的线程即使抢到了
CPU 执行权,也只能等待,等待锁的释放。
代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争抢锁,谁抢到谁就能运行同步代码块。
同步代码块
因此,修改后的代码为:
package test.MyThread.ticketDemo;
public class RunnableThread implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run(){
while(true){
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + " 张票");
ticket--;
}
}
}
}
}
package test.MyThread.ticketDemo;
public class ticketDemo1 {
public static void main(String[] args) {
//这里没有改动,只是在上一个代码中加了一把锁
RunnableThread r1 = new RunnableThread();
Thread t1 = new Thread(r1,"窗口一");
Thread t2 = new Thread(r1,"窗口二");
Thread t3 = new Thread(r1,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
可以看出来结果符合我们的预期,是正确的
现在又有了新的问题,那就是如果我在构造线程的 RunnableThread 类里面加入方法呢?同步代码块里面出现方法时,我们应该怎么“上锁”呢?
同步方法(this 锁)
同步方法,在 public 的后面加上 synchronized 关键字
package test.MyThread.ticketDemo;
public class RunnableThread1 implements Runnable{
private int ticket = 100;
Object obj = new Object();
public boolean flag = true;
@Override
public void run(){
if(flag==true){
while(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
SellTicket1();
}
}
}
//同步方法,在 public 的后面加上 synchronized 关键字
public synchronized void SellTicket1(){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 张票");
ticket--;
}
}
}
package test.MyThread.ticketDemo;
public class ticketDemo2 {
public static void main(String[] args) throws InterruptedException {
RunnableThread1 r = new RunnableThread1();
Thread t1 = new Thread(r,"窗口一");
Thread t2 = new Thread(r,"窗口二");
t1.start();
t2.start();
}
}
this 锁
先来看看,如果有两条路径,一条路径是使用同步代码块,但是对象是 obj,另一条路径是使用同步方法
package test.MyThread.ticketDemo;
public class TicketWindow2 implements Runnable{
//定义 100 张票
private static int tickets = 100;
Object obj = new Object();
int i =0;
@Override
public void run() {
while (true){
if(i%2==0){
synchronized (obj){
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");
}
}
}else {
sellTicket();
}
i++;
}
}
public synchronized void sellTicket(){
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");
}
}
}
结果出错,说明同步方法的用的对象锁不能是任意的对象,不同的线程应该用相同的锁。同步方法是属于对象,而在这个类里面调用方法的是 this 对象,也就是 this.sellTicket(),因此把 this 提取出来作为对象锁中的对象。这样多个线程都用的是 this 锁
package test.MyThread.ticketDemo;
public class TicketWindow2 implements Runnable{
//定义 100 张票
private static int tickets = 100;
Object obj = new Object();
int i =0;
@Override
public void run() {
while (true){
if(i%2==0){
synchronized (this){
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");
}
}
}else {
sellTicket();
}
i++;
}
}
public synchronized void sellTicket(){
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票");
}
}
}
修改完成后再运行代码,发现没有错误
注:
一个线程使用同步方法,另一个线程使用同步代码块 this 锁,可以实现同步
一个线程使用同步方法,另一个线程使用同步代码块,但是不是 this 锁。这种情况不能实现同步。
静态同步方法
同步方法的锁对象是 this,
静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件
下面代码挺长的,但其实就修改了上面同步方法的代码的两处地方
评论