一线程创建的两种方式比较
线程创建和启动有两种方式,这里只是列出步骤,不再进行详细解释。
(1)继承Thread类
class MyThread extends Thread{ public void run(){ ... } } MyThread mt=new MyThread();//创建线程 mt.start();//启动线程
(2)实现Runnable接口
class MyThread implements Runnable{ public void run(){ ... } } MyThread mt=new MyThread(); Thread td=new Thread(mt);//创建线程 td.start();//启动线程
(3)两种方式的比较
1)Runnable方式可以避免Thread方式由于Java单继承特性带来的缺陷。
2)Runnable方式的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况。
二模拟应用场景
模拟一个火车站买票的场景,某车次还剩下5张火车票,有三个窗口去卖这5张火车票,我们使用三个线程模拟三
个窗口同时卖这5张火车票,我们看Thread方式和Runnable方式这两种方式模拟出一个什么样的结果。
(1)使用Thread方式模拟买票
TicketsThread.java源文件代码:
class MyThread extends Thread{ //一共有五张火车票 private int ticketsCount = 5; //窗口,也就是线程的名字 private String name; //构造方法 public MyThread(String name){ this.name = name; } public void run(){ while(ticketsCount > 0){ //如果还有票,就卖掉一张 ticketsCount--; System.out.println(name+"卖了1张票,剩余票数为:"+ticketsCount); } } } public class TicketsThread{ public static void main(String[] args){ //创建三个线程,模拟三个窗口买票 MyThread mt1 = new MyThread("窗口1"); MyThread mt2 = new MyThread("窗口2"); MyThread mt3 = new MyThread("窗口3"); //启动三个线程,也就是窗口开始卖票 mt1.start(); mt2.start(); mt3.start(); } }
运行结果:
得到的结果并不是我们想要的结果。
在票的数量加static修饰关键字得到的结果是正确的。这里不再进行演示。
(2)使用Runnable方式模拟买票
TicketsRunnable.java源文件代码:
class MyThread1 implements Runnable{ //一共有五张火车票 private int ticketsCount = 5; public void run(){ while(ticketsCount > 0){ //如果还有票,就卖掉一张 ticketsCount--; System.out.println(Thread.currentThread().getName()+"卖了1张票,剩余票数为:"+ticketsCount); } } } public class TicketsRunnable{ public static void main(String[] args){ MyThread1 mt = new MyThread1(); //创建三个线程,模拟三个窗口买票 Thread th1 = new Thread(mt,"窗口1"); Thread th2 = new Thread(mt,"窗口2"); Thread th3 = new Thread(mt,"窗口3"); //启动三个线程,也就是窗口开始卖票 th1.start(); th2.start(); th3.start(); } }
得到了预期的结果:
(3)结果分析
由于线程的执行是随机的,打印的结果也是随机的。
Thread方式
三个线程,创建了三个Thread对象,每个线程都有自己的Thread对象,都有自己的ticketsCount变量,它们三个
线程并不是共享ticketsCount变量,也就是每个线程都可以卖出5张火车票,即三个窗口卖出去15张火车票。
Runnable方式
三个线程共用一个Runnable对象,也就是三个线程共用一个ticketsCount变量,即三个窗口一共卖了5张火车票。
前者不是一个线程三个对象,是三个Thread对象,也是三个线程,这三个线程启动后都会执行5次卖票,实现不
了共享“5张票”这个资源,所以输出就会有15张票卖出去,显然不符合实际,用Runnable就可以解决这个问题,创建
的三个线程可以共享"5张票"这个资源。
ticketsCont变量是实例变量,它的值自然是存在堆中(每个java对象在堆中都会占据一定内存,而实例变量的值就
是存储在这块内存中,类似于结构体,因此每个对象对应一个ticketsCont的值),ticketsCont跟值传递没有关系啊,如
果是Runnable方式的话,传递的也只是MyThread对象引用的副本,不管ticketsCont的事,但是因为ticketsCont的值
在引用和引用副本所指向的堆内存中,所以无论是引用还是引用副本改变了堆内存中ticketsCont的值,都会产生效
果!
这个根据你的需要来操作,这样说吧,如果有一个比较大的资源要你下载,那么你用Thread方式那么你就只能一
个线程去吧这个资源下载完,如果是runable方式的话你就可以 多new几个子线程来出来,通过共享runable对象里面
的资源来用多个子线程来下载这个资源,这样的话,下载资源的时候 runable方法会使下载的线程多一些几率在cpu里
面,也会让你下载速度变快继承Thread类是多个线程分别完成自己的任务,实现Runnable接口是多个线程共同完成
一个任务。
三线程的生命周期
线程的生命周期转换示意图:
线程的生命周期:
1)创建:新建一个线程对象,如Thread thd=new Thread()。
2)就绪:创建了线程对象后,调用了线程的start()方法(注意:此时线程只是进入了线程队列,等待获取CPU服
务,具备了运行的条件,但并不一定已经开始运行了)。
3)运行:处于就绪状态的线程,一旦获取了CPU资源,便进入到运行状态,开始执行run()方法里面的逻辑。
4)阻塞:一个正在执行的线程在某些情况下,由于某种原因而暂时让出了CPU资源,暂停了自己的执行,便进入
了阻塞状态,如调用了sleep()方法。
5)终止:线程的run()方法执行完毕,或者线程调用了stop()方法,线程便进入终止状态。
这里我们可以用一个经典的线问题就是生产者和消费者问题的实例:
import java.util.*; public class ProducerConsumer{ public static void main(String[] args){ SyncStack ss = new SyncStack(); Producer p = new Producer(ss); Consumer c = new Consumer(ss); new Thread(p).start(); new Thread(c).start(); } } //生产与消费对象 class WoTou{ int id; //构造方法 WoTou(int id){ this.id = id; } //重写toString()方法 public String toString(){ return "WoTou : " + id; } } //容器类 class SyncStack{ int index = 0; WoTou[] arrWT = new WoTou[4]; public synchronized void push(WoTou wt){ while(index == arrWT.length){ try{ //这里的wait()方法指的是Object类中的方法 this.wait(); }catch(InterruptedException e){ e.printStackTrace(); } } //这里的notifyAll()方法指的是唤醒所有线程,而notify()方法唤醒一个线程 this.notifyAll(); arrWT[index] = wt; index ++; } public synchronized WoTou pop(){ while(index == 0){ try{ //这里的wait()方法指的是Object类中的方法 this.wait(); }catch(InterruptedException e){ e.printStackTrace(); } } //这里的notifyAll()方法指的是唤醒所有线程,而notify()方法唤醒一个线程 this.notifyAll(); index--; return arrWT[index]; } } //生产者 class Producer implements Runnable{ SyncStack ss = null; Producer(SyncStack ss){ this.ss = ss; } public void run(){ for(int i=0; i<20; i++){ WoTou wt = new WoTou(i); ss.push(wt); System.out.println("生产了:" + wt); try{ Thread.sleep((int)(Math.random() * 200)); }catch(InterruptedException e){ e.printStackTrace(); } } } } //消费者 class Consumer implements Runnable{ SyncStack ss = null; Consumer(SyncStack ss){ this.ss = ss; } public void run(){ for(int i=0; i<20; i++){ WoTou wt = ss.pop(); System.out.println("消费了: " + wt); try{ Thread.sleep((int)(Math.random() * 1000)); }catch(InterruptedException e){ e.printStackTrace(); } } } }
运行结果:
关于一些问题的解析:
执行线程sleep()方法是依然占着cpu的,操作系统认为该当前线程正在运行,不会让出系统资源。
执行wait()方法是让线程到等待池等待,让出一系列的系统资源,其他线程可以根据调度占用cpu线程的资源有不
少,但应该包含CPU资源和锁资源这两类。
sleep(long mills):让出CPU资源,但是不会释放锁资源。
wait():让出CPU资源和锁资源。
锁是用来线程同步的,sleep(long mills)虽然让出了CPU,但是不会让出锁,其他线程可以利用CPU时间片了,但
如果其他线程要获取sleep(long mills)拥有的锁才能执行,则会因为无法获取锁而不能执行,继续等待。但是那些没有
和sleep(long mills)竞争锁的线程,一旦得到CPU时间片即可运行了。
四守护线程
(1)守护线程理论知识
Java线程分为两类:
1)用户线程:运行在前台,执行具体的任务。程序的主线程、连接网络的子线程等都是用户线程。
2)守护线程:运行在后台,为其他前台线程服务。守护线程的特点是一旦所有用户线程都结束运行,守护线程会
随JVM一起结束工作。守护线程的应用:数据库连接池中的检测线程和JVM虚拟机启动后的检测线程等等。最常见的
守护线程:垃圾回收线程
设置守护线程:
可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程。
使用守护线程的注意事项:
1)setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常。
2)在守护线程中产生的新线程也是守护线程。
3)不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
(2)守护线程代码示例:
import java.io.*; import java.util.*; class DaemonThread implements Runnable{ public void run(){ System.out.println("进入守护线程"+Thread.currentThread().getName()); try{ writeToFile(); }catch(Exception e){ e.printStackTrace(); } System.out.println("退出守护线程"); } private void writeToFile() throws Exception{ File filename = new File("E:\\Java\\JavaSE\\Thread"+File.separator+"daemon.txt"); OutputStream os = new FileOutputStream(filename,true); int count = 0; while(count < 999){ os.write(("\r\nword"+count).getBytes()); System.out.println("守护线程"+Thread.currentThread().getName()+"向文件中写入了word"+ count++); Thread.sleep(1000); } } } public class DaemonThreadDemo{ public static void main(String[] args){ System.out.println("进入主线程"+Thread.currentThread().getName()); DaemonThread daemonThread = new DaemonThread(); Thread thread =new Thread(daemonThread); thread.setDaemon(true); thread.start(); Scanner sc = new Scanner(System.in); sc.next(); System.out.println("退出主线程"+Thread.currentThread().getName()); } }
运行结果:
(3)使用jstack生成线程快照
作用:生成JVM当前时刻线程的快照(threaddump,即当前进程中所有线程的信息)。
目的:帮助定位程序问题出现的原因,如长时间停顿、CPU占用率高等。
jstack命令行工具
你安装JDK安装目录下的bin文件夹下:
位置:E:\Java\develop\jdk1.8.0_25\bin
如何使用jstack
在cmd中输入:jstack
我们去任务管理器中找到一个进程的PID:
生成快照:
五总结
(1)怎样解决死锁的问题
1)尽量避免不必要的synchronized关键字。
2)可以用其他方法替换synchronized关键字,比如标志不可变量。
3)保证synchronized代码块简练。
(2)创建线程的建议
根据两种创建线程方法的比较,得出的结论是使用实现Runnable接口的方法创建的多线程更好一些,另外,就是
需要重点注意,程序中的同一个资源指的是同一个Runnable对象。建议多使用Runnable这种方式创建多线程。
(3)补充:
1)程序中的同一资源指的是同一个Runnable对象。
2)安全的卖票程序中需要加入同步(Synchronized)。我们的代码过程中并没有加入,如果需要完善,就必须加入
同步锁。