多线程
线程的创建
1.继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码
2.实现java.lang.Runnablr接口,同样是在run()方法中实现运行在线程上的代码
继承Thread类创建多线程
public class Main {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.run();
while(true){
System.out.println("main run");
}
}
}
class MyThread{
public void run(){
while (true){
System.out.println("mythread run");
}
}
}
从运行结果可以看出,程序一直打印“MyThread类的run()方法在运行”,这是因为该程序是一个单线程程序,第4行代码调用MyThread类的run()方法时,遇到第12~14行代码定义的死循环中,循环会一直进行。因此,MyThread类的打印语句将被无限执行,而main()方法中的打印语句无法得到执行。
该程序是一个单线程程序,Mythread类的打印语句将会被无限执行,而main()方法中的打印语句将无法得到执行
想要两个while循环中的能够并发执行,就需要实现多线程
为此Java中提供了一个线程类Thread,通过继承Thread类,并且重修Thread类中的run()方法便可以实现多线程
在Thread中,提供了一个start()方法用于启动新线程,线程启动后,虚拟机会自动调用run()方法,如果子类重写了该方法便会执行子类中的方法
public class Main {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();//执行线程中run()
while(true){
System.out.println("main run");
}
}
}
class MyThread extends Thread{
public void run(){
while (true){
System.out.println("mythread run");
}
}
}
上述代码中,第5~7行代码定义了一个while循环,并在while循环中死循环打印“main()方法在运行”; 第12~14行代码也定义了一个死循环while,并在循环中打印“MyThread类的run()方法在运行”。利用两个while来模拟多线程环境,从运行结果,可以看到两个循环中的语句都有输出,说明该文件实现了多线程。
单线程和多线程的区别
从上图可以看出,单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main()方法和MyThread类的run()方法却可以同时运行,互不影响,这正是单线程和多线程的区别。
实现Runnable接口创建多线程
继承Thread类具有局限性:Java只支持单继承,一个类一旦继承其他父类就无法再继承Thread类,进而无法创建线程
Java提供了另一种方法:Thread(Runnable target)
其中Runnable是一个接口,他只有一个run()方法
当使用该方法创建线程对象时只需要为该方法传递一个实现了Runnable接口的实例对象
这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不用调用Thread类中的run()方法
public class Main {
public static void main(String[] args) {
MyThread myThread=new MyThread();//创建MyThread的实例对象(实现了Runnable接口的对象
Thread thread=new Thread(myThread);//创建线程对象,将myThread对象传入
thread.start();//执行线程中run()
while (true)
{
System.out.println("main run");
}
}
}
class MyThread implements Runnable{//实现了Runnable接口的类
@Override
public void run() {
while (true)
{
System.out.println("mythread run");
}
}
}
上述代码中,第1117行代码定义的MyThread类实现了Runnable接口,并在第1216行代码中重写了Runnable接口中的run()方法,在第4行代码中通过Thread类的构造方法将MyThread类的实例对象作为参数传入,在第5行代码中使用start()方法开启MyThread线程,最后在第6~8行代码中定义了一个while死循环。从运行结果可以看出,main()方法和run()方法中的打印语句都执行了,说明文件8-3实现了多线程。
两种实现方式对比:
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程。
为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称。
1.使用Thread类实现
public class Main {
public static void main(String[] args) {
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
}
}
class TicketWindow extends Thread {
private int tickets=100;
public void run(){
while (true){
if(tickets>0){
Thread th=Thread.currentThread();
String name=th.getName();
System.out.println(name+" "+tickets--);
}
}
}
}
从运行结果可以看出,每张票都被打印了四次。出现这样现象的原因是四个线程没有共享100张票,而是各自出售了100张票。
在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源。
需要注意的是,上述程序中每个线程都有自己的名字,主线程默认的名字是“main”,用户创建的第一个线程的名字默认为“Thread-0”,第二个线程的名字默认为“Thread-1”,以此类推。如果希望指定线程的名称,可以通过调用setName(String name)方法为线程设置名称。
为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式。
2.使用Runnable接口实现
public class Main {
public static void main(String[] args) {
TicketWindow tw=new TicketWindow(); // 创建TicketWindow实例对象tw
new Thread(tw,"1").start();// 创建线程对象并命名为窗口1,开启线程
new Thread(tw,"2").start();
new Thread(tw,"3").start();
new Thread(tw,"4").start();
}
}
class TicketWindow implements Runnable {
private int tickets=100;
public void run(){
while (true){
if(tickets>0){
Thread th=Thread.currentThread();// 获取当前线程
String name=th.getName(); // 获取当前线程的名字
System.out.println(name+" "+tickets--);
}
}
}
}
上述程序中,第10~21行代码创建了一个TicketWindow对象并实现了Runnable接口,然后在mian方法中创建了四个线程,在每个线程上都去调用这个TicketWindow对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。
实现Runnable接口相对于继承Thread类来说,具有以下优势:
(1)适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
(2)可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,因此不能使用继承Thread类的方式,只能采用实现Runnable接口的方式。
简化创建方式
在创建线程时指定线程要调用的方法
1 public class Main {
2 public static void main(String[] args) {
3 Thread t = new Thread(() -> {
4 while (true){
5 System.out.println("start new thread!");
6 }
7 });
8 t.start(); // 启动新线程
9 }
10 }
11 class MyThread extends Thread {
12 public void run() {
13 while (true) { // 通过死循环语句打印输出
14 System.out.println("MyThread类的run()方法在运行");
15 }
16 }
17 }
线程的生命周期及状态转换
1.新建状态(New)
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
2.就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
3.运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
4.阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
下面就列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。
● 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
● 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
● 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
● 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
● 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
5.死亡状态(Terminated)
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
线程调度
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。
线程优先级
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级,如下表。
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如,main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。
public class Main {
public static void main(String[] args) {
Thread min=new Thread(new Max(),"min");
Thread max=new Thread(new Min(),"max");
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);
min.start();
max.start();
}
}
class Max implements Runnable{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
}
}
}
class Min implements Runnable{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
}
}
}
上述代码中,第28行代码定义了MaxPriority类并实现了Runnable接口,第1016行代码定义实现了Runnable接口的MinPriority类,并在MaxPriority类与MinPriority类中使用for循环打印正在发售的票数,在第22行代码中使用MIN_PRIORITY方法设置minPriority线程的优先级为1,在第23行代码中使用MAX_PRIORITY方法设置manPriority线程优先级为10。从运行结果可以看出,优先级较高的maxPriority线程先运行了,运行完毕后优先级较低的minPriority线程才开始运行。所以优先级越高的线程获取CPU切换时间片的机率越大。
线程休眠sleep()
如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(单位毫秒)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。
sleep方法声明会抛出InterruptedException异常,一次在调用该方法时应该捕获异常,或者声明抛出该异常
1 public class Example07 {
2 public static void main(String[] args) throws Exception {
3 // 创建一个线程
4 new Thread(new SleepThread()).start();
5 for (int i = 1; i <= 10; i++) {
6 if (i == 5) {
7 Thread.sleep(2000); // 当前线程休眠2秒
8 }
9 System.out.println("主线程正在输出:" + i);
10 Thread.sleep(500); // 当前线程休眠500毫秒
11 }
12 }
13 }
14 // 定义SleepThread类实现Runnable接口
15 class SleepThread implements Runnable {
16 public void run() {
17 for (int i = 1; i <= 10; i++) {
18 if (i == 3) {
19 try {
20 Thread.sleep(2000); // 当前线程休眠2秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 System.out.println("SleepThread线程正在输出:" + i);
26 try {
27 Thread.sleep(500); // 当前线程休眠500毫秒
28 } catch (Exception e) {
29 e.printStackTrace();
30 }
31 }
32 }
33 }
上述代码中,第1531行代码定义了一个SleepThread类并实现了Runnable接口。在SleepThread类中重写了run()方法,run()方法中使用for循环打印线程输出语句;第2630行代码使用sleep()方法设置线程休眠500毫秒,在第1824行代码中使用if判断当变量i=3时,线程休眠2000毫秒;第4行中使用new关键词创建了一个SleepThread线程并启动,在第512行代码中使用for循环打印主线程的输出语句,并在第10行代码使用sleep()方法设置线程休眠500毫秒,在第6~8行代码中使用if判断当变量i=5时,线程休眠2000毫秒。
在主线程与SleepThread类线程中分别调用了Thread的sleep(500)方法让其线程休眠,目的是让一个线程在打印一次后休眠500毫秒,从而使另一个线程获得执行的机会,这样就可以实现两个线程的交替执行。
从运行结果可以看出,主线程输出2后,SleepThread类线程没有交替输出3,而是主线程接着输出了3和4,这说明了当i等于3时,SleepThread类线程进入了休眠等待状态。对于主线程也一样,当i等于5时,主线程会休眠2000毫秒。
注意:sleep时静态方法,只能控制当前正在运行的线程休眠,当休眠结束后,线程就会返回到就绪状态,而不是立即开始执行
线程让步yield()
所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行。
线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,只是将线程转换成就绪状态,让系统的调度器重新调度一次,当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会
1 // 定义YieldThread类继承Thread类
2 class YieldThread extends Thread {
3 // 定义一个有参的构造方法
4 public YieldThread(String name) {
5 super(name); // 调用父类的构造方法
6 }
7 public void run() {
8 for (int i = 0; i < 6; i++) {
9 System.out.println(Thread.currentThread().getName() + "---" + i);
10 if (i == 3) {
11 System.out.print("线程让步:");
12 Thread.yield(); // 线程运行到此,作出让步
13 }
14 }
15 }
16 }
17 public class Example08 {
18 public static void main(String[] args) {
19 // 创建两个线程
20 Thread t1 = new YieldThread("线程A");
21 Thread t2 = new YieldThread("线程B");
22 // 开启两个线程
23 t1.start();
24 t2.start();
25 }
26 }
在上述代码中,第2021行代码中创建了两个线程t1和t2,它们的优先级相同。在814行代码的for循环中线程在变量i等于3时,调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行,从运行结果可以看出,当线程B输出3以后,会做出让步,线程A继续执行,同样,线程A输出3后,也会做出让步,线程B继续执行。
线程插队join()
现实生活中经常能碰到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。
当某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续执行
1 public class Example09{
2 public static void main(String[] args) throws Exception {
3 // 创建线程
4 Thread t = new Thread(new EmergencyThread(),"线程一");
5 t.start(); // 开启线程
6 for (int i = 1; i < 6; i++) {
7 System.out.println(Thread.currentThread().getName()+"输入:"+i);
8 if (i == 2) {
9 t.join(); // 调用join()方法
10 }
11 Thread.sleep(500); // 线程休眠500毫秒
12 }
13 }
14 }
15 class EmergencyThread implements Runnable {
16 public void run() {
17 for (int i = 1; i < 6; i++) {
18 System.out.println(Thread.currentThread().getName()+"输入:"+i);
19 try {
20 Thread.sleep(500); // 线程休眠500毫秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 }
26 }
在上述代码中,在第4行代码中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。
案例Svip优先办理服务
在日常工作生活中,无论哪个行业都会设置一些Svip用户,Svip用户具有超级优先权,在办理业务时,Svip用户具有最大的优先级。
本案例要求编写一个模拟Svip优先办理业务的程序,在正常的业务办理中,插入一个Svip用户,优先为Svip用户办理业务。本案例在实现时,可以通过多线程实现。
多线程同步
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
前面讲解的售票案例,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。接下来对售票案例进行修改,模拟四个窗口出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠10毫秒。
1 public class Example10 {
2 public static void main(String[] args) {
3 SaleThread saleThread = new SaleThread(); // 创建SaleThread对象
4 // 创建并开启四个线程
5 new Thread(saleThread, "线程一").start();
6 new Thread(saleThread, "线程二").start();
7 new Thread(saleThread, "线程三").start();
8 new Thread(saleThread, "线程四").start();
9 }
10 }
11 // 定义SaleThread类实现Runnable接口
12 class SaleThread implements Runnable {
13 private int tickets = 10; // 10张票
14 public void run() {
15 while (tickets > 0) {
16 try {
17 Thread.sleep(10); // 经过此处的线程休眠10毫秒
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 System.out.println(Thread.currentThread().getName() + "---卖出的票"
22 + tickets--);
23 }
24 }
25 }
在上述代码中,第1225行代码定义了一个SaleThread类并实现了Runnable接口,第13行代码定义了总票数为10,第1424行代码重写了run()方法,在run()方法中使用while循环售票,并在第17行代码中添加了sleep()方法休眠线程10毫秒,用于模拟售票过程中线程的延迟,最后在第3~8行代码的中创建并开启四个线程。用于模拟四个售票窗口。
在运行结果中,最后打印售出的票出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。
出现这样的安全问题的原因是在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。
线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,必须得保证在任何时刻只能有一个线程访问共享资源。具体示例如下:
while (tickets > 0) {
try {
Thread.sleep(10); // 经过此处的线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"+ tickets--);
}
同步代码块
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){
操作共享资源代码块
}
上面的格式中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
1 //定义Ticket1类继承Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10; // 定义变量tickets,并赋值10
4 Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
5 public void run() {
6 while (true) {
7 synchronized (lock) { // 定义同步代码块
8 try {
9 Thread.sleep(10); // 经过的线程休眠10毫秒
10 } catch (InterruptedException e) {
11 e.printStackTrace();
12 }
13 if (tickets > 0) {
14 System.out.println(Thread.currentThread().getName()
15 + "---卖出的票" + tickets--);
16 } else { // 如果 tickets小于0,跳出循环
17 break;
18 }
19 }
20 }
21 }
22 }
23public class Example11 {
24 public static void main(String[] args) {
25 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
26 // 创建并开启四个线程
27 new Thread(ticket, "线程一").start();
28 new Thread(ticket, "线程二").start();
29 new Thread(ticket, "线程三").start();
30 new Thread(ticket, "线程四").start();
31 }
32 }
上述代码中,将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。运行结果中并没有出现线程二和线程三售票的语句,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程三始终未获得锁对象,所以未能显示它们的输出结果。
注意同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。
同步方法
同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized 返回值类型 方法名([参数1,...]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
1 // 定义Ticket1类实现Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10;
4 public void run() {
5 while (true) {
6 saleTicket(); // 调用售票方法
7 if (tickets <= 0) {
8 break;
9 }
10 }
11 }
12 // 定义一个同步方法saleTicket()
13 private synchronized void saleTicket() {
14 if (tickets > 0) {
15 try {
16 Thread.sleep(10); // 经过的线程休眠10毫秒
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(Thread.currentThread().getName() + "---卖出的票"
21 + tickets--);
22 }
23 }
24 }
25 public class Example12 {
26 public static void main(String[] args) {
27 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
28 // 创建并开启四个线程
29 new Thread(ticket,"线程一").start();
30 new Thread(ticket,"线程二").start();
31 new Thread(ticket,"线程三").start();
32 new Thread(ticket,"线程四").start();
33 }
34 }
上述代码中,第12~23行代码将售票代码抽取为售票方法saleTicket(),并用synchronized关键字把saleTicket()修饰为同步方法,然后在第6行代码中调用saleTicket()。从图8-16所示的运行结果可以看出,同样没有出现0号和负数号的票,说明同步方法实现了和同步代码块一样的效果。
同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果。
同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
死锁问题
结果可想而知,两个人都吃不到饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
1 class DeadLockThread implements Runnable {
2 static Object chopsticks = new Object(); // 定义Object类型的chopsticks锁对象
3 static Object knifeAndFork = new Object(); // 定义Object类型的knifeAndFork锁对象
4 private boolean flag; // 定义boolean类型的变量flag
5 DeadLockThread(boolean flag) { // 定义有参的构造方法
6 this.flag = flag;
7 }
8 public void run() {
9 if (flag) {
10 while (true) {
11 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
12 System.out.println(Thread.currentThread().getName()
13 + "---if---chopsticks");
14 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
15 System.out.println(Thread.currentThread().getName()
16 + "---if---knifeAndFork");
17 }
18 }
19 }
20 }
21 } else {
22 while (true) {
23 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
24 System.out.println(Thread.currentThread().getName()
25 + "---else---knifeAndFork");
26 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
27 System.out.println(Thread.currentThread().getName()
28 + "---else---chopsticks");
29 }
30 }
31 }
32 }
33 }
34 }
35 public class Example13 {
36 public static void main(String[] args) {
37 // 创建两个DeadLockThread对象
38 DeadLockThread d1 = new DeadLockThread(true);
39 DeadLockThread d2 = new DeadLockThread(false);
40 // 创建并开启两个线程
41 new Thread(d1, "Chinese").start(); // 创建开启线程Chinese
42 new Thread(d2, "American").start(); // 创建开启线程American
43 }
44 }
在上述代码中,第133行代码的DeadLockThread类中创建了Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。第1019行代码中设置Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕;第21 ~30行代码中设置American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕。两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了死锁。