当前位置:   article > 正文

线程与同步_如果在某个接口内,再新建一个线程,那么新建的线程内可以获取到原来线程内的thread

如果在某个接口内,再新建一个线程,那么新建的线程内可以获取到原来线程内的thread

1.Thread的常用方法和总结

1.1.通过getName()获取当前线程的名称

要获取当前线程的名称,首先要获取当前线程对象:

  • 在MyThread类(继承Thread的方式)中获取当前线程对象比较简单,直接在run方法中通过this就可以
    因为MyThread类就是线程类,创建的MyThread对象就是线程对象,而this表示当前对象,因此当通过MyThread对象调用run方法时,run方法中的this表示调用该方法的对象,也就是当前线程对象
    在MyThread类中获取当前线程对象以及输出线程名称如下
//2.重写Thread类中的run()方法
public void run() {
	//3.编写业务代码: 通过for循环输出5次“MyThread.run()方法执行了...”
	for( int i=1; i<=5; i++ ) {
		System.out.print("MyThread.run()方法执行了..."+i);
		System.out.println("..."+this.getName() );
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 但在MyRunnable类(实现Runnable接口的方式)中获取当前线程对象比较麻烦,必须使用Thread.currentThread()方法来获取当前线程对象
    因为MyRunnable实现了Runnable接口,并不是线程类,创建的MyThread对象也不是线程对象。
    因此在run方法中通过this获取的当前对象也不是线程对象。这种方式中创建的Thread对象才是线程对象,可以通过Thread类中的currentThread()静态方法获取当前线程对象
    在MyRunnable类中获取当前线程对象以及输出线程名称如下
//2.重写Runnable接口中的run()方法
public void run() {
	//3.编写业务代码: 通过for循环输出5次“MyRunnable.run()方法执行了...”
	for (int i=0; i < 5; i++) {
		System.out.print( "MyRunnable.run()方法执行了..."+i );
		System.out.println( "..."+Thread.currentThread().getName() );
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

1.2.通过setName()设置当前线程的名称

上面获取的线程名称都是有系统默认指定的名称,如果要设置线程名称可以通过构造方法,在创建线程时指定线程名称:

Thread() 
	-- 创建新的 Thread 对象,由系统指定默认的线程名称。 
Thread(String name) 
	-- 创建新的 Thread 对象,并指定线程名称:name。 
Thread(Runnable target) 
	-- 创建新的 Thread 对象,该线程对象负责执行target中的run方法
	-- 由系统指定默认的线程名称。 
Thread(Runnable target, String name) 
	-- 创建新的 Thread 对象,该线程对象负责执行target中的run方法
	-- 并指定线程名称:name
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

代码示例:

class MyThread extends Thread{
	public MyThread() { super(); }
	public MyThread(String name) { super(name); }
	...
}
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread( "one" ); //指定线程名称
mytd.start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

或者通过setName()方法指定线程的名称:

//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread();
//MyThread mytd = new MyThread( "one" ); //指定线程名称
mytd.setName( "two" );
  • 1
  • 2
  • 3
  • 4

1.3.run()方法的作用

该方法中包含了线程体, 也就是线程执行的代码
在创建线程对象后,通过 线程对象.start 方法将线程转为就绪状态, 当线程获得cpu执行权时就会进入运行状态,线程运行其实运行的就是 run方法中的代码。
既然如此,可以将开启线程的代码( mytd.start() )改为( mytd.run() )吗?
下面以继承Thread类实现多线程的方式进行测试:

//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread();
mytd.setName( "two" );
//mytd.start();
mytd.run();
  • 1
  • 2
  • 3
  • 4
  • 5

运行结果:

MyThread.run()方法执行了...1...two
MyThread.run()方法执行了...2...two
MyThread.run()方法执行了...3...two
MyThread.run()方法执行了...4...two
MyThread.run()方法执行了...5...two
主方法main()方法执行了...1
主方法main()方法执行了...2
主方法main()方法执行了...3
主方法main()方法执行了...4
主方法main()方法执行了...5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

经过多次测试,结果显示程序中其实只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有实现多线程交替执行的目的。
以实现Runnable接口实现多线程的方式进行测试,效果和上面一样
造成以上的原因是:

  • 通过查看start()方法的源码发现:当执行start方法时(启动一个线程时),首先会判断线程的状态(threadStatus)是否为0(新建状态),如果不是0,就会抛出非法线程状态异常(IllegalThreadStateException);如果为0,则将该线程加入线程组,最后尝试调用start0()方法,该方法是私有的native方法,调用后线程就转为就绪状态,等待分配cpu的执行权。
  • 而查看run()方法的源码:当执行run方法时,首先会判断target是否不等于null,如果不为null,则执行target中的run方法,target其实就是一个Runnable接口的子类对象,直接调用Runnable子类对象的run方法,就和调用一个普通的方法没什么区别,是不会创建线程的
    结论: 通过线程对象调用run方法,尽管该方法可以让线程体中的代码执行,但并不能创建线程,和调用普通方法没什么区别。

1.4.两种实现多线程方式的区别

既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方式在实际应用中又有什么区别呢?

  • 在ThreadTest类中定义MyThread类,是通过继承Thread类实现多线程,由于Java是单继承,所以当MyThread类继承了Thread类后,就无法再继承别的类,这种方式造成了一定的局限性。
  • 在ThreadTest2类中定义的MyThread类,是通过实现Runnable接口实现多线程,MyThread即使实现了Runnable接口,也不影响继承其他类以及实现其他接口,这种方式没有局限性。
  • 在ThreadTest类中创建的MyThread类的对象就是线程对象:
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread(); //Thread的子类
td.start();	
  • 1
  • 2
  • 3
  • 而在ThreadTest2类中创建的Runnable子类对象( target )并不是线程对象,只是作为线程对象的target(执行目标),Thread对象才是线程对象,Thread线程对象负责执行 target对象中的run方法
//4.创建MyRunnable的实例对象(不是线程对象, 是线程对象的执行目标)
MyRunnable target = new MyRunnable(); //Runnable的实现类
//5.创建Thread类的实例(线程对象),将MyRunnable的实例传入,并调用start()方法启动子线程
Thread thread = new Thread( target );
thread.start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 在MyThread类(继承Thread的方式)中获取当前线程对象比较简单,直接在run方法中通过this就可以;但在MyRunnable类(实现Runnable接口的方式)中获取当前线程对象比较麻烦,必须使用Thread.currentThread()方法来获取当前线程对象;
  • 实现Runnable接口实现多线程,适合多个任务代码相同的线程处理同一个资源的情况

2.多线程售票案例

假设售票厅有3个窗口可以发售某日某次列车的100张车票。
其中这100张车票可以作为共享资源,3个售票窗口可以通过创建3个线程进行模拟
下面我们就分别通过继承Thread类的方式以及实现Runnable接口的方式实现多线程售票案例

2.1.继承Thread类实现多线程售票

实现思路: 创建一个测试类 cn.tedu.thread_3.TicketTest1
1.定义一个Ticket类(线程类),并让该类继承Thread类
2.定义一个成员变量tickets,用于存储所卖票的数量
int tickets = 100;
3.重写Thread类中的run方法
4.编写业务代码: 通过一个死循环进行售票
4.1.只要剩余票数大于0,则继续售票
4.2.每卖出一张票后,剩余票数要减去1
4.3.直到剩余票数不大于0(小于或等于0)就停止循环退出卖票
5.测试:创建3个Ticket线程对象:t1,t2,t3, 并分别调用start()方法启动线程
代码实现如下:

public class TicketTest1 {
	public static void main(String[] args) {
		//5.测试: 创建3个Ticket线程对象: t1, t2, t3, 并分别调用start()方法启动线程
		Ticket t1 = new Ticket();
		t1.setName( "窗口 1" ); 
		Ticket t2 = new Ticket();
		t2.setName( "窗口 2" );
		Ticket t3 = new Ticket();
		t3.setName( "窗口 3" );
		t1.start();
		t2.start();
		t3.start();	
	}
}
// 1.定义一个Ticket类(线程类),并让该类继承Thread类
class Ticket extends Thread{
	//2.定义一个成员变量tickets,用于存储所卖票的数量
	int tickets = 100;
	//3.重写Thread类中的run方法
	public void run() {
		//4.编写业务代码: 通过一个死循环进行售票
		while( true ) {
			//4.1.只要剩余票数大于0,则继续售票
			if( tickets > 0 ) {
				//4.2.每卖出一张票后,剩余票数要减去1
				System.out.println( getName()+" 线程..卖出了第"+( tickets-- )+"张票" );
			}else { 
				//4.3.直到剩余票数不大于0(小于或等于0)就停止循环退出卖票
				break; 
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

运行结果如下:

窗口 1 线程..卖出了第100张票
窗口 1 线程..卖出了第99张票
窗口 2 线程..卖出了第100张票
窗口 2 线程..卖出了第99张票
窗口 1 线程..卖出了第97张票
窗口 2 线程..卖出了第98张票
窗口 3 线程..卖出了第100张票
窗口 3 线程..卖出了第99张票
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

方式1中的问题
从上面的运行结果中可以看出: 100张票在每个窗口中都输出了一次。
出现这种情况的原因是: Ticket类中有一个成员变量: tickets=100,接着在程序中创建了3个Ticket对象(相当于3个售票程序),每个Ticket对象都拥有一个tickets变量,每个tickets变量都有100张票(3个线程对象一共300张票)
在卖票的过程中,每个线程对象(t1,t2,t3)在独自处理各自的100张票,归根结底,是因为这3个线程没有共享这100票导致的。
因此上面的运行结果显然是不合理的,因为在现实中铁路系统中的火车票资源是共享的
为了保证这100张票被所有线程所共享,将Ticket类中的 tickets 变量改为静态变量(用static修饰),被static修饰的成员变量叫做类成员,类成员不属于任何一个对象,而是只属于类,但可以被所有对象共享。
由于Ticket类只有一个,tickest变量也就只有一个,就是100张票,这样一来,即使创建了3个线程,访问的都是Ticket类中的那100张票(而不是300张票)
将上面Ticket类中的 tickets 变量代码修改为如下:

static int tickets = 100; 
  • 1

再次运行结果如下:

窗口 1..卖出第100张票
窗口 3..卖出第98张票
窗口 2..卖出第99张票
窗口 3..卖出第96张票
窗口 1..卖出第97张票
窗口 3..卖出第94张票
窗口 2..卖出第95张票
窗口 3..卖出第92张票
窗口 1..卖出第93张票
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.2.实现Runnable接口实现多线程售票案例

实现思路:
1.定义Ticket类, 并实现Runnable接口
2.定义成员变量tickets,用于存储所卖票的数量
int tickets = 100;
3.重写Runnable接口中的run()方法
4.编写业务代码: 通过死循环进行售票
4.1.只要剩余票数大于0,则继续卖票
4.2.每次卖出一张票后,剩余票数要减去1
4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
5.创建Ticket的实例对象(不是线程对象, 是线程对象的执行目标)
6.创建3个线程对象t1,t2,t3,并分别调用start()方法启动线程
代码实现如下:

/*
 - 通过实现Runnable接口实现多线程售票案例
 */
public class TicketTest2 {
	public static void main(String[] args) {
		//5.创建Ticket的实例对象(不是线程对象, 是线程对象的执行目标)
		Ticket target = new Ticket();
		//6.创建3个线程对象t1,t2,t3,并分别调用start()方法启动线程
		Thread t1 = new Thread( target , "窗口 1" );
		Thread t2 = new Thread( target , "窗口 2" );
		Thread t3 = new Thread( target , "窗口 3" );
		t1.start();
		t2.start();
		t3.start();
	}
}
//1.定义Ticket类, 并实现Runnable接口
class Ticket implements Runnable {
	//2.定义成员变量tickets,用于存储所卖票的数量
	int tickets = 100;
	//3.重写Runnable接口中的run()方法
	public void run() {
		//4.编写业务代码: 通过死循环进行售票
		while(true) {
			//4.1.只要剩余票数大于0,则继续卖票
			if( tickets>0 ) {
				//4.2.每次卖出票后,剩余票数要减去1
				System.out.println( Thread.currentThread().getName()
									+"..卖出第"+(tickets--)+"张票" );
				//tickets--;
			}else {
				//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
				break;
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下:

窗口 1..卖出第100张票
窗口 3..卖出第98张票
窗口 1..卖出第97张票
窗口 1..卖出第96张票
窗口 2..卖出第99张票
窗口 1..卖出第94张票
窗口 3..卖出第95张票
窗口 1..卖出第92张票
窗口 2..卖出第93张票
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从上面的运行结果中可以看出: 3个窗口依次输出了这100张票,没有出现重复,说明3个线程访问的是同一个tickets变量,共享了100张车票。
之所以没有产生3个线程各自出售100张票,因为执行目标只有一个(Ticket target)对象,这个对象里的tickets只有一份,只有100张票,即使创建3个线程,访问的也只是这100张票。

2.3.多线程售票案例的安全问题

上面通过继承Thread类和实现Runable接口分别实现了多线程售票案例,尽管最后程序的执行结果中显示多个线程依次卖出了100张票,没有任何问题,但其实程序中隐藏了很大的安全问题,只需要将程序稍加修改,就可以看出这些问题。
继承Thread类和实现Runable接口的方式中都存在这样的问题,下面选择一种进行测试:
将4.2处的一行代码改为两行,如下所示:

public void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		//4.1.只要剩余票数大于0,则继续卖票
		if( tickets>0 ) {
			//4.2.每次卖出票后,剩余票数要减去1
			System.out.println( Thread.currentThread().getName()
								+"..卖出第"+ tickets +"张票" );
			tickets--;
		}else {
			//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
			break;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

执行结果:

窗口 1..卖出第100张票	-- 重复卖票
窗口 3..卖出第100张票	-- 重复卖票
窗口 1..卖出第99张票
窗口 2..卖出第100张票	-- 重复卖票
窗口 1..卖出第97张票
窗口 3..卖出第98张票
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在4.1和4.2中间位置添加如下代码:

public void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		//4.1.只要剩余票数大于0,则继续卖票
		if( tickets>0 ) {
			//线程执行到这一行,将会休眠20ms(毫秒)
			try {
				Thread.sleep( 20 );
			} catch (InterruptedException e) {
				e.printStackTrace();
			}	
			//4.2.每次卖出票后,剩余票数要减去1
			System.out.println( Thread.currentThread().getName()
								+"..卖出第"+ tickets +"张票" );
			tickets--;
		}else {
			//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
			break;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

执行结果:

窗口 2..卖出第3张票
窗口 1..卖出第2张票
窗口 3..卖出第1张票
窗口 2..卖出第0张票	-- 超出卖票
窗口 1..卖出第-1张票	-- 超出卖票
  • 1
  • 2
  • 3
  • 4
  • 5

多线程售票案例的安全问题总结:

  • 售票案例很可能出现一张票被卖出多次或者出现负数的情况,这些情况都是因为[多个线程]操作[共享资源tickets]所导致的线程安全问题
  • 当其中一个线程1(比如窗口1线程)执行完下面这行代码时(假设tickets=90,即卖出了第90张票),失去了CPU的执行权,而线程2(比如窗口2线程)获得了CPU的执行权,也执行了下面这行代码(也卖出了第90时张票),此时就出现了多个线程卖同一张票的情况
 System.out.println(Thread.currentThread().getName()+"..卖出第"+tickets+"张票"); 
  • 1
  • 当其中一个线程1(比如窗口1线程)执行到 if( tickets>0 ),假设此时tickets=1(即还剩最后一张票),由于tickets>0,因此进入if语句块中,但停在了位置1处, 如果此时线程1恰好失去了CPU的执行权,而线程2(比如窗口2线程)获得了CPU的执行权,当执行到 if( tickets>0 ),由于此时tickets=1, 由于tickets>0,因此线程2也进入到了if语句块中停在了位置1处,接着继续执行卖票操作,卖出了第1张票,并执行tickets–(tickets变为0),接着线程1又获得了CPU的执行权,也执行了卖票操作,卖出了第0张票,并执行tickets–,此时就出现了超出买票的情况
  if( tickets>0 ) {
	//...位置1处,在此处让线程sleep一段时间(比如20毫秒)
	System.out.println(Thread.currentThread().getName()+"..卖出第"+tickets+"张票");
	tickets--;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 判断一个程序是否有线程安全问题的依据:
    a: 是否存在多线程环境(即多个线程并发执行)
    b: 是否存在共享数据(同一个成员变量可以被多个线程访问)
    c: 是否存在多条语句访问、修改共享数据
    要解决多线程实现售票案例中的线程安全问题,就必须使用同步

3.同步

3.1.同步代码块概述

同步是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行。
Java为线程的同步操作提供了synchronized关键字,使用该关键字修饰的代码块被称为同步代码块。
其语法格式如下:

synchronized( 同步对象 ){
	需要同步的代码;
}
  • 1
  • 2
  • 3

注意: 在使用同步代码块时必须指定一个需要同步的对象,也称为锁对象,这里的锁对象可以是任意对象。但多个线程必须使用同一个锁对象。

3.2.同步代码块的使用

下面使用同步代码块解决上面的多线程售票程序中的线程安全问题
需要注意的是:

  1. 将有可能发生线程安全问题的代码包含在同步代码块中,同一时间只允许一个线程进入同步代码块中
  2. synchronized代码块中的锁对象可以是任意对象,但必须只能是一个锁。
    若使用this作为锁对象,需保证多个线程执行时,this指向的是同一个对象

3.3.下面以继承Thread类的多线程售票程序为例,使用[同步代码块]的代码如下:

//3.重写Thread类中的run()方法
static Object obj = new Object();
public void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		/* 使用同步代码块: 
		 *1)将有可能发生线程安全问题的代码包含在同步代码块中,同一时间只允
		 *许一个线程进入同步代码块中
		 *2)synchronized代码块的锁对象可以是任意对象,但必须只能是一个锁。
		 *若使用this作为锁对象,需保证多个线程执行时,this指向是同一个对象
		 *这里不能使用this,因为this表示的锁对象有: t1,t2,t3,锁对象有3个,仍有安全问题
		 *可以在当前类中声明一个静态引用指向一个对象,因为静态引用指向的对象属于类,只有一份 */
		//synchronized ( this ) { 
		synchronized ( obj ) {
			//4.1.只要剩余票数大于0,则继续卖票
			if( tickets>0 ) {
				//线程执行到这一行,将会休眠20ms(毫秒)
				try {
					Thread.sleep( 20 );
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//4.2.每次卖出票后,剩余票数要减去1
				System.out.println( Thread.currentThread().getName()
										+"..卖出第"+ tickets +"张票" );
				tickets--;
			}
		}
		//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
		if( tickets <= 0 )
			break;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

3.4.下面以实现Runnable接口的多线程售票程序为例,使用[同步代码块]的代码如下:

//3.重写Runnable接口中的run()方法
public void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		/* 使用同步代码块: 
		* 1) 将有可能发生线程安全问题的代码包含在同步代码块中,同一时间只允许
		* 	一个线程进入同步代码块中
		* 2) synchronized代码块中的锁对象可以是任意对象,但必须只能是一个锁。
		* 	若使用this作为锁对象,需保证多个线程执行时,this指向的是同一个对象 */
		synchronized ( this ) {
			//4.1.只要剩余票数大于0,则继续卖票
			if( tickets>0 ) {
				//线程执行到这一行,将会休眠20ms(毫秒)
				try {
					Thread.sleep( 20 );
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//4.2.每次卖出票后,剩余票数要减去1
				System.out.println( Thread.currentThread().getName()
										+"..卖出第"+ tickets +"张票" );
				tickets--;
			}
		}
		//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
		if( tickets <= 0 )
			break;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

4.同步方法

4.1.概述

除了可以将需要的代码设置成同步代码块以外,也可以使用synchronized关键字将一个方法修饰成同步方法,它能实现和同步代码块同样的功能。
具体语法格式如下:

权限修饰符 synchronized 返回值类型/void 方法名([参数1,...]){
	需要同步的代码;
}
  • 1
  • 2
  • 3

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
需要注意的是,同步方法的锁是当前调用该方法的对象,也就是this指向的对象。

4.2.同步方法的使用

下面使用同步方法解决上面的多线程售票程序中的线程安全问题
需要注意的是:

  1. 将有可能发生线程安全问题的方法使用synchronized修饰,同一时间只允许一个线程进入同步方法中
  2. synchronized方法的锁对象是当前调用该方法的对象,也就是this指向的对象。
    如果当前方法是非静态方法,this表示的是调用当前方法的对象
    如果当前方法的静态方法,this表示的是当前类。

4.3.下面以继承Thread类的多线程售票程序为例,使用[同步方法]的代码如下:

/*
* 使用同步方法:
* 1) 将有可能发生线程安全问题的方法使用synchronized修饰,同一时间只允许
* 	一个线程进入同步方法中
* 2) synchronized方法的锁对象是当前调用该方法的对象,也就是this指向的对象。
*如果当前方法是非静态方法,this表示的是调用当前方法的对象
*如果当前方法的静态方法,this表示的是当前类。
* 如果将synchronized加在run方法上,锁对象是this,当前方法是非静态方法
*this对应的锁是: t1,t2,t3对象,锁对象有3个,因此还是有线程安全问题
*/
//3.重写Thread类中的run()方法
public synchronized void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		//4.1.只要剩余票数大于0,则继续卖票
		if( tickets>0 ) {
			//线程执行到这一行,将会休眠20ms(毫秒)
			try {
				Thread.sleep( 20 );
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//4.2.每次卖出票后,剩余票数要减去1
			System.out.println( Thread.currentThread().getName()
								+"..卖出第"+ tickets +"张票" );
			tickets--;
		}
		//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
		if( tickets <= 0 )
			break;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

解决方法是:

  1. 声明一个静态方法(比如sellTicket), 用synchronized修饰为同步方法,将run方法中的所有代码放在该方法中,并在run方法中调用该方法
  2. 由于是静态方法,this表示的是当前类,锁对象只有1个,因此不会有线程安全问题
public void run() {
	sellTicket();
}
/*
* 解决方法: 
* 	1) 声明一个静态方法(比如sellTicket), 用synchronized修饰为同步方法
* 		将run方法中的所有代码放在该方法中,并在run方法中调用该方法
* 	2) 由于是静态方法,this表示的是当前类,锁对象只有一个,所以不会有线程安全问题
*/
public static synchronized void sellTicket() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		//4.1.只要剩余票数大于0,则继续卖票
		if( tickets>0 ) {
			//线程执行到这一行,将会休眠20ms(毫秒)
			try {
				Thread.sleep( 20 );
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//4.2.每次卖出票后,剩余票数要减去1
			System.out.println( Thread.currentThread().getName()
						+"..卖出第"+ tickets +"张票" );
			tickets--;
		}
		//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
		if( tickets <= 0 )
			break;
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

4.4.下面以实现Runnable接口的多线程售票程序为例,使用[同步方法]的代码如下:

/*
* 使用同步方法:
* 1) 将有可能发生线程安全问题的方法使用synchronized修饰,同一时间只允许
* 	一个线程进入同步方法中
* 2) synchronized方法的锁对象是当前调用该方法的对象,也就是this指向的对象。
*如果当前方法是非静态方法,this表示的是调用当前方法的对象
*如果当前方法的静态方法,this表示的是当前类。
*如果将synchronized加在run方法上,锁对象是this,当前方法是非静态方法
*this对应的锁是 target对象,锁对象只有1个,因此没有线程安全问题
*/
//3.重写Runnable接口中的run()方法
public synchronized void run() {
	//4.编写业务代码: 通过死循环进行售票
	while(true) {
		//4.1.只要剩余票数大于0,则继续卖票
		if( tickets>0 ) {
			//线程执行到这一行,将会休眠20ms(毫秒)
			try {
				Thread.sleep( 20 );
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//4.2.每次卖出票后,剩余票数要减去1
			System.out.println( Thread.currentThread().getName()
					+"..卖出第"+ tickets +"张票" );
			tickets--;
		}
		//4.3.直到剩余票数不大于0(小于或等于0)就停止循环,退出卖票
		if( tickets <= 0 )
			break;
	}
}










 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/144680
推荐阅读
相关标签
  

闽ICP备14008679号