赞
踩
要获取当前线程的名称,首先要获取当前线程对象:
//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() );
}
}
//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() );
}
}
上面获取的线程名称都是有系统默认指定的名称,如果要设置线程名称可以通过构造方法,在创建线程时指定线程名称:
Thread()
-- 创建新的 Thread 对象,由系统指定默认的线程名称。
Thread(String name)
-- 创建新的 Thread 对象,并指定线程名称:name。
Thread(Runnable target)
-- 创建新的 Thread 对象,该线程对象负责执行target中的run方法
-- 由系统指定默认的线程名称。
Thread(Runnable target, String name)
-- 创建新的 Thread 对象,该线程对象负责执行target中的run方法
-- 并指定线程名称:name
代码示例:
class MyThread extends Thread{
public MyThread() { super(); }
public MyThread(String name) { super(name); }
...
}
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread( "one" ); //指定线程名称
mytd.start();
或者通过setName()方法指定线程的名称:
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread();
//MyThread mytd = new MyThread( "one" ); //指定线程名称
mytd.setName( "two" );
该方法中包含了线程体, 也就是线程执行的代码
在创建线程对象后,通过 线程对象.start 方法将线程转为就绪状态, 当线程获得cpu执行权时就会进入运行状态,线程运行其实运行的就是 run方法中的代码。
既然如此,可以将开启线程的代码( mytd.start() )改为( mytd.run() )吗?
下面以继承Thread类实现多线程的方式进行测试:
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread();
mytd.setName( "two" );
//mytd.start();
mytd.run();
运行结果:
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
经过多次测试,结果显示程序中其实只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有实现多线程交替执行的目的。
以实现Runnable接口实现多线程的方式进行测试,效果和上面一样
造成以上的原因是:
既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方式在实际应用中又有什么区别呢?
//4.创建一个线程对象mytd,并调用start()方法启动子线程
MyThread mytd = new MyThread(); //Thread的子类
td.start();
//4.创建MyRunnable的实例对象(不是线程对象, 是线程对象的执行目标)
MyRunnable target = new MyRunnable(); //Runnable的实现类
//5.创建Thread类的实例(线程对象),将MyRunnable的实例传入,并调用start()方法启动子线程
Thread thread = new Thread( target );
thread.start();
假设售票厅有3个窗口可以发售某日某次列车的100张车票。
其中这100张车票可以作为共享资源,3个售票窗口可以通过创建3个线程进行模拟
下面我们就分别通过继承Thread类的方式以及实现Runnable接口的方式实现多线程售票案例
实现思路: 创建一个测试类 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 线程..卖出了第100张票
窗口 1 线程..卖出了第99张票
窗口 2 线程..卖出了第100张票
窗口 2 线程..卖出了第99张票
窗口 1 线程..卖出了第97张票
窗口 2 线程..卖出了第98张票
窗口 3 线程..卖出了第100张票
窗口 3 线程..卖出了第99张票
...
方式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..卖出第100张票
窗口 3..卖出第98张票
窗口 2..卖出第99张票
窗口 3..卖出第96张票
窗口 1..卖出第97张票
窗口 3..卖出第94张票
窗口 2..卖出第95张票
窗口 3..卖出第92张票
窗口 1..卖出第93张票
实现思路:
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..卖出第100张票
窗口 3..卖出第98张票
窗口 1..卖出第97张票
窗口 1..卖出第96张票
窗口 2..卖出第99张票
窗口 1..卖出第94张票
窗口 3..卖出第95张票
窗口 1..卖出第92张票
窗口 2..卖出第93张票
从上面的运行结果中可以看出: 3个窗口依次输出了这100张票,没有出现重复,说明3个线程访问的是同一个tickets变量,共享了100张车票。
之所以没有产生3个线程各自出售100张票,因为执行目标只有一个(Ticket target)对象,这个对象里的tickets只有一份,只有100张票,即使创建3个线程,访问的也只是这100张票。
上面通过继承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..卖出第100张票 -- 重复卖票
窗口 3..卖出第100张票 -- 重复卖票
窗口 1..卖出第99张票
窗口 2..卖出第100张票 -- 重复卖票
窗口 1..卖出第97张票
窗口 3..卖出第98张票
在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; } } }
执行结果:
窗口 2..卖出第3张票
窗口 1..卖出第2张票
窗口 3..卖出第1张票
窗口 2..卖出第0张票 -- 超出卖票
窗口 1..卖出第-1张票 -- 超出卖票
多线程售票案例的安全问题总结:
System.out.println(Thread.currentThread().getName()+"..卖出第"+tickets+"张票");
if( tickets>0 ) {
//...位置1处,在此处让线程sleep一段时间(比如20毫秒)
System.out.println(Thread.currentThread().getName()+"..卖出第"+tickets+"张票");
tickets--;
}
同步是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行。
Java为线程的同步操作提供了synchronized
关键字,使用该关键字修饰的代码块被称为同步代码块。
其语法格式如下:
synchronized( 同步对象 ){
需要同步的代码;
}
注意: 在使用同步代码块时必须指定一个需要同步的对象,也称为锁对象,这里的锁对象可以是任意对象。但多个线程必须使用同一个锁对象。
下面使用同步代码块解决上面的多线程售票程序中的线程安全问题
需要注意的是:
//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; } }
//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; } }
除了可以将需要的代码设置成同步代码块以外,也可以使用synchronized关键字将一个方法修饰成同步方法,它能实现和同步代码块同样的功能。
具体语法格式如下:
权限修饰符 synchronized 返回值类型/void 方法名([参数1,...]){
需要同步的代码;
}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
需要注意的是,同步方法的锁是当前调用该方法的对象,也就是this指向的对象。
下面使用同步方法解决上面的多线程售票程序中的线程安全问题
需要注意的是:
/* * 使用同步方法: * 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; } }
解决方法是:
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) 将有可能发生线程安全问题的方法使用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; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。