版权声明:
作者:WangJY
出处:http://www.imrookie.cn
本文版权归作者所有,转载请指明出处,否则保留追究法律责任的权利。
1.前言
Java使用Thread类代表线程,所有的线程都必须是Thread或其子类的实例。
执行代码写在run()方法中。
使用start()方法启动线程。
线程的生命周期:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
2.常用方法
方法 | 描述 |
run() | 线程执行体,写线程实际执行的代码 |
start() | 启动线程,当线程处于新建状态时调用 |
Thread.sleep(x) | 当前线程睡眠x毫秒 |
yield() | 线程让步,将该线程转为就绪状态 |
Thread.currentThread() | 获取当前线程 |
isAlive() | 判断当前线程是否活着,线程处于就绪,运行和阻塞时返回true,新建和死亡返回false |
join() | 在线程执行中间插入另外一个线程,当前线程进入阻塞状态,执行完某线程的join()方法后,线程回复为就绪状态; |
join(x) | 等待被join的线程最长时间为x毫秒,超时则不再等待 |
setDaemon(true) | 将线程设置为守护线程 |
isDaemon() | 判断线程是否为守护线程 |
3.创建方式
方法一:继承Thread类创建线程;
测试代码:
package com.rookie.topic.test; /** * 多线程测试 */ public class ThreadTest extends Thread { /** * 重写父类的run()方法 */ public void run(){ //getName()获取该线程的名字 System.out.println("测试线程["+this.getName()+"]启动..."); for(int i=0;i<3;i++){ System.out.println("测试线程["+this.getName()+"]正在执行-"+i); } System.out.println("测试线程["+this.getName()+"]执行完毕!"); } public static void main(String[] args){ //创建10个测试线程 for(int i=0;i<10;i++){ new ThreadTest().start();//创建实例后调用start()方法来执行该线程 } } }
方法二:实现Runnable接口创建线程;
测试代码:
package com.rookie.topic.test; /** * 多线程测试: 实现Runnable接口创建线程 */ public class ThreadTest2 implements Runnable { /** * 实现接口的run方法 */ @Override public void run() { /** * 注意这里的区别 * 在实现类中没有getName()方法,所以要使用Thread.currentThread()来获取当前线程 */ System.out.println(Thread.currentThread().getName() + "线程执行..."); } public static void main(String[] args){ //1.创建实现类实例 ThreadTest2 tt = new ThreadTest2(); /** * 将实现类实例作为参数创建Thread实例,并执行 * 参数2为线程名字,也可以使用new Thread(tt),不传入名字 */ for(int i=0;i<10;i++){ new Thread(tt,"name_"+i).start(); } } }
4.线程的生命周期
1.新建:当使用new关键字创建线程对象后,该线程处于新建状态;
2.就绪:当线程对象调用了start()方法后,该线程处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器;
3.运行:处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,该线程处于运行状态;失去CPU资源,线程会回到就绪状态,或者调用yield()方法使线程转为就绪状态;
4.阻塞:对于采用抢占式调度策略的系统(现代的桌面和服务器操作系统)而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间段用完,系统就会剥夺该线程所占据的资源,让其他线程获得执行的机会;在采用协作式调度策略的系统(小型设备如有些手机)中,只有当一个线程调用sleep()方法或yield()方法后才会放弃所占用的资源,也就是必须由该线程主动放弃所占用的资源。
当发生如下情况,线程将会进入阻塞状态:
线程调用sleep方法主动放弃所占用的处理器资源;
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
线程在等待某个通知;
程序调用了线程的suspend方法将该线程挂起。不过这个方法容易导致死锁,所以程序应该尽量避免使用该方法;
对应的解除阻塞的办法:
调用sleep方法的线程经过了指定时间;
线程调用的阻塞式IO方法已经返回;
线程成功的取得了同步监视器;
线程等到了通知;
处于挂起状态的线程被调用了resume方法恢复执行;
线程结束阻塞后会进入就绪状态
5.死亡:线程以以下三种方式结束后处于死亡状态;
1.run()方法执行完成,线程正常结束;
2.线程抛出一个未捕获的Exception或Error;
3.调用该线程的stop方法来结束该线程(该方法不安全,不推荐);
4.使用interrupt()方法中断线程
5.守护线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
特征:如果所有的前台线程都死亡,后台线程会自动死亡。
通过setDarmon(true)可以将线程设置为守护线程,要在start()方法执行前设置;
isDaemon()判断该线程是否为守护线程;
6.线程睡眠、线程让步
使用Thread.sleep()使当前线程睡眠(抱着锁睡眠,所以容易引起死锁),进入阻塞状态,当睡眠时间过去后,线程进入就绪状态。
使用yield()方法将该线程暂停,转为就绪状态,将执行机会让给和自己优先级同级或者更高优先级的线程。
7.线程优先级
每个线程执行时都有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程获得较少的机会。
每个线程的默认优先级都与创建它的线程优先级相同,默认情况下,main线程的优先级为普通。
getPriority()获取线程优先级
setPriority(int a)设置优先级,取值为[1,10];
优先级常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)
应该尽量避免使用自定义的数字作为优先级,要使用MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY静态常量来设置优先级。
线程让步与优先级测试代码:
package com.rookie.topic.test; /** * 线程测试:线程让步与线程优先级 */ public class ThreadTest3 extends Thread { /** * 线程执行体 */ public void run(){ for(int i=0;i<10;i++){ System.out.println("当前线程名字: "+this.getName()+" ; 线程优先级为: "+this.getPriority()); if(i==5 && this.getPriority() == 10){ System.out.println("_____高优先级线程["+this.getName()+"]执行让步..."); this.yield(); } } } /** * 带参构造函数,为线程起名字 * @param name */ public ThreadTest3(String name){ super(name); } public static void main(String[] args){ System.out.println("主线程"+Thread.currentThread().getName()+"的优先级为: "+Thread.currentThread().getPriority()); ThreadTest3 t1 = new ThreadTest3("高高高"); t1.setPriority(MAX_PRIORITY); ThreadTest3 t2 = new ThreadTest3("低低低"); t2.setPriority(MIN_PRIORITY); t1.start(); t2.start(); } }
8.线程同步
当多个线程共享同一个资源时,就可能发生“错误情况”。
无同步处理示例:
package com.rookie.topic.test; /** * 线程同步测试类 */ public class ThreadTest4 extends Thread { private User user; //构造器 public ThreadTest4(User user){ this.user = user; } public void run(){ if(this.user.getMoney() > 800){ System.out.println("钱还剩"+this.user.getMoney()+",够了,取800!"); this.user.setMoney(this.user.getMoney() - 800); }else{ System.out.println("钱不够..."); } System.out.println("钱还剩"+user.getMoney()); } public static void main(String[] args){ User user = new User(1000); //模拟对同一个账户并发操作 new ThreadTest4(user).start(); new ThreadTest4(user).start(); } } /** * 同步测试模型 */ class User{ private int money; public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } //构造器 public User(int money){ this.money = money; } }
执行结果:
钱还剩1000,够了,取800! 钱还剩1000,够了,取800! 钱还剩200 钱还剩-600
显然,错误发生了,余额产生了负值。
解决办法:
1.同步代码块
在共享资源操作处添加同步监视器
示例:
public void run(){ /** * 同步代码块 * 括号里是需要同步的对象,多线程下,同时最多只有一个线程可以进入同步代码块, * 一个线程在请求同步监视器时如果同步监视器正在被另一个线程所占用,则该请求线程为阻塞状态,直到获取到对应的同步监视器(取到后会转为就绪状态) */ synchronized(this.user){ if(this.user.getMoney() > 800){ System.out.println("钱还剩"+this.user.getMoney()+",够了,取800!"); this.user.setMoney(this.user.getMoney() - 800); }else{ System.out.println("钱不够..."); } System.out.println("钱还剩"+user.getMoney()); } }
2.同步方法
与同步代码块类似,使用synchronized修饰的方法就是同步方法,同步监视器为this,即当前对象本身。
与同步代码块一样,当有线程正在执行同步方法时,其他线程无法执行该方法,为阻塞状态,直到获取到同步监视器时转为就绪状态。
示例:
package com.rookie.topic.test; /** * 线程同步测试类 */ public class ThreadTest4 extends Thread { private User user; //构造器 public ThreadTest4(User user){ this.user = user; } public void run(){ //调用同步方法 this.user.pay(); } public static void main(String[] args){ User user = new User(1000); //模拟对同一个账户并发操作 new ThreadTest4(user).start(); new ThreadTest4(user).start(); } } /** * 同步测试模型 */ class User{ private int money; //构造器 public User(int money){ this.money = money; } //对象本身提供同步方法供外部调用 public synchronized void pay(){ if(this.money > 800){ System.out.println("钱还剩"+this.money+",够了,取800!"); this.money = this.money - 800; }else{ System.out.println("钱不够..."); } System.out.println("钱还剩"+this.money); } }
线程同步是以牺牲性能为代价的,所以不要每个方法都用synchronized关键字修饰,只对那些会改变共享资源的方法同步。
如果某方法有时用在单线程环境,有时用在多线程环境,就拆分为2个方法,各用各的。
同步监视器释放锁定的几种情况:
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
当线程在同步代码块、同步方法中遇到break、return终止了该代码块(方法)的继续执行,当前线程将会释放同步监视器。
当前线程在同步代码块(方法)中出现了未处理的Error或Exception,导致了该代码块(方法)异常结束时会释放同步监视器。
当线程执行同步代码块(方法)时,程序执行了同步监视器对象的wait()方法,则当前线程进入等待(阻塞状态),并释放同步监视器。
注意,在下面情况下,线程不会释放同步监视器:
线程执行同步代码块(方法)时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
线程执行同步代码块(方法)时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用suspend和resume方法来控制线程。
3.同步锁(Lock)
通过锁对象来对资源进行加解锁。
锁对象更灵活,可以在代码里面合适的行进行加解锁。解锁条件判断更灵活。
/** * 同步测试模型 */ class User{ //锁对象 private final ReentrantLock lock = new ReentrantLock(); private int money; public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } //构造器 public User(int money){ this.money = money; } //对象本身提供同步方法供外部调用 public /*synchronized*/ void pay(){ lock.lock();//加锁 try{ if(this.money > 800){ System.out.println("钱还剩"+this.money+",够了,取800!"); this.money = this.money - 800; }else{ System.out.println("钱不够..."); } }finally{ lock.unlock();//解锁 } System.out.println("钱还剩"+this.money); } }
9.死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁。
sleep()会抱着锁进行睡眠
10.安全关闭线程
可以使用标志位或者interrupt()中断
interrupt中断类似于标志位,可以用isInterrupted()判断是否被中断过
但是当线程结束后中断标识会被清除,或者当sleep方法中抛出中断异常后也会清除中断标志.