Java多线程编程核心技术1——Thread类核心API
Thread类相关状态和方法示意图:
线程状态和方法示意图一. Thread类核心API使用
1. 进程是一次程序的执行,可以理解成Windows任务管理器的一个exe程序;线程是进程中独立运行的子任务。
2. 实现多线程编程有两种方式:
2.1 继承Thread类,覆盖run()。(Thread类也实现了Runnaable接口)
优点:如需访问当前线程,无需使用Thread.currentThread()方法。
缺点:已继承Thread类,不能再继承其他父类。
2.2 实现Runnable接口来创建(必须封装到Thread类)。
优点:还可以继承其他父类,适合多个相同线程来处理同一份资源。
缺点:如需访问当前线程,必须使用Thread.currentThread()方法。
使用多线程时,代码的运行结果与代码执行顺序或调用顺序无关。
3.currentThread()方法
Thread.currentThread().getName():返回代码段正在被哪个线程调用的信息。
4.isAlive():判断线程是否处于活动状态。
package pgsthread;
public class TestAlive extends Thread{
public TestAlive(){
System.out.println("---TestAlive begins---");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()="+Thread.currentThread().isAlive());
System.out.println("this.getName()="+this.getName());
System.out.println("this.isAlive()="+this.isAlive());
System.out.println("---TestAlive ends---");
}
@Override
public void run(){
System.out.println("---run begins---");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()="+Thread.currentThread().isAlive());
System.out.println("this.getName()="+this.getName());
System.out.println("this.isAlive()="+this.isAlive());
System.out.println("---run ends---");
}
}
package pgsthread;
public class Run {
public static void main(String[] args) {
TestAlive t = new TestAlive();
Thread t1 = new Thread(t);
//Thread(Runnable target):可以传入一个Thread类的对象,将t的run()交给t1执行。
System.out.println("main begin t1 isAlive="+t1.isAlive());
t1.setName("A");
t1.start();
System.out.println("main end t1 isAlive="+t1.isAlive());
}
}
运行结果如下:
---TestAlive begins---
Thread.currentThread().getName()=main
Thread.currentThread().isAlive()=true
this.getName()=Thread-0
this.isAlive()=false
---TestAlive ends---
main begin t1 isAlive=false
main end t1 isAlive=true
---run begins---
Thread.currentThread().getName()=A
Thread.currentThread().isAlive()=true
this.getName()=Thread-0
this.isAlive()=false
---run ends---
此处的Thread.currentThread()指向t1,this指向t。
5.判读线程是否是停止状态?
import pgsthread.TestAlive;
public class TestInterrupt {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); //true
System.out.println(Thread.interrupted()); //false
TestAlive t = new TestAlive();
t.start();
t.interrupt();
System.out.println(t.isInterrupted()); //true
System.out.println(t.isInterrupted()); //true
}
}
Thread.currentThread().interrupt();中断的是当前线程,即main。
this.interrupted():具有将状态位清楚为false的功能。
this.isInterrupted():不清除标志位。
6.主动行为——暂停线程使之阻塞
suspend():暂停线程;resume():恢复线程的执行。
废弃上述两个方法的原因:
(1) 具有独占性,若在同步代码块中调用suspend()而一直未调用resume(),会锁死该同步代码块。
public void println(long x){
synchronized (this){
print(x);
newline();
}
}
如果在进入println(long x)内部是调用suspend(),将锁定该方法,同步锁将不释放。
(2) 不同步,导致线程不安全。
会导致同方法suspend()后的代码得不到执行。
7.线程优先级
setPriority():优先级分为1~10级,默认为5。
(1)线程的优先级具有继承性。
(2)优先级具有规则性:高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。
(3) 优先级具有随机性,并不一定优先级高的线程就一定先执行完。ps,线程优先级与代码中出现的先后顺序无关。
8.守护线程 Thread.setDaemon(true)
Java线程分为两种,一种是用户线程,另一种是守护(Daemon)线程。当进程中没有用户线程时,守护线程也将自动销毁。
二. 线程间通信
1.等待/通知(wait/notify)机制
(1) 不使用等待/通知机制
使用sleep()结合while(true)死循环轮询检测,会浪费CPU资源。即两个线程主动式的读取一个共享变量,在花费读取时间的基础上,读到的值不一定是想要的。
(2) 等待/通知机制:
wait()使线程停止运行,notify()使停止的线程继续运行(随机挑选出一个呈wait状态的线程)。
在调用wait()和notify()前必须获得该对象的对象级别锁,即只能在同步方法或者代码块中调用wait()和notify()方法。
※要等到notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才释放锁,而不是notify()后马上唤醒wait状态的线程进行操作。
(3) sleep():不释放锁;notify():不立即释放锁;wait():释放锁。
(4) 不管线程是wait()还是sleep()状态,调用interrupt()都会抛出InterruptedException异常。只有运行的线程才可被interrupt()。
(5) 唤醒所有等待中的线程:
方法一:多次调用notify();
方法二:notifyAll();
(6) 等待wait的条件发生变化:
※用while去判断,while是轮询的,同步的,每一次都会进行判断;而if的话可能有两个线程进去同时进入if语句块就会产生非线程安全问题。
2.生产者/消费者模式实现
多个生产者与消费者:采用wait/notify机制,但不一定每一次唤醒的都是异类,连续唤醒同类就会产生“假死”,以下加粗的地方则为连续唤醒同类。
生产者1 +1
生产者1 wait
生产者2 wait
消费者1 1-1=0
消费者1 wait
消费者2 wait
生产者1 +1
生产者1 wait
生产者2 wait
解决方案1:改为notifyAll();
3. 通过管道进行线程间通信
:一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
3.1 字节流:PipedInputStream和PipedOutputStream
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream();
out.connect(in);或 in.connect(out);//使两个管道之间进行通信链接
3.2 字符流:PipedReader和PipedWriter,同上connect
4.实例 -- 等待/通知の交叉备份 -- 使线程具有有序性(设置一个volatile参数)
package threadservice;
public class DBTools {
volatile private boolean flag = false;
synchronized public void saveA(){
while(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("save A!");
flag = true;
notifyAll();
}
synchronized public void saveB(){
while(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("save B!");
flag = false;
notifyAll();
}}
package threadservice;
public class ThreadA extends Thread{
private DBTools dbtools;
public ThreadA(DBTools dbtools){
this.dbtools = dbtools;
}
@Override
public void run() {
dbtools.saveA();
}
}
package threadservice;
public class ThreadB extends Thread{
private DBTools dbtools;
public ThreadB(DBTools dbtools){
this.dbtools = dbtools;
}
@Override
public void run() {
dbtools.saveB();
}
}
package threadservice;
public class Run {
public static void main(String[] args) {
DBTools dbtools = new DBTools();
for(int i = 0; i < 3;i++){
ThreadA tA = new ThreadA(dbtools);
tA.start();
ThreadB tB = new ThreadB(dbtools);
tB.start();
}
}
}
打印结果就是交叉打印的,循环等待和通知所有wait线程,但是只有flag正确的那个线程才能运行,即想要顺序执行的话,只要设置参数就可以。
save A!
save B!
save A!
save B!
save A!
save B!
5.join()
比如子线程要进行大量的耗时计算,主线程往往早于子线程结束。当主线程需要等待子线程完成之后再结束,就需要用到join()。
如在t1中调用t2.join();t1暂停,必须等到t2运行完才能继续运行。即等待线程对象销毁。
(1) join与synchronized区别:
join()在内部使用wait()方法进行等待,而synchronized使用的是“对象监视器”原理作为同步。
(2) 如果当前线程对象被中断,则当前线程出现异常(只影响当前线程)。
方法join()与interrupt()方法彼此遇到,会出现异常。但影响的是当前的线程,其他线程正常运行。
(3) join(long)和sleep(long)的区别
join(long):在内部使用的是wait(long),具有释放锁地特点。在内部执行wait(long)后,当前线程的锁被释放,其他线程就可以调用此线程中的同步方法了。
而sleep(long)不释放锁。
(4) 经过运行如下代码发现join后面的代码有可能提前运行,可能会出现以下结果:
package jointhread;
public class ThreadA extends Thread {
private ThreadB b;public ThreadA(ThreadB b) {
super();
this.b = b;
}@Override
public void run() {
try {
synchronized (b) {
System.out.println("A begins at " + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("A ends at " + System.currentTimeMillis());
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package jointhread;
public class ThreadB extends Thread{
@Override
synchronized public void run() {
try {
System.out.println("B begins at " + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("B ends at " + System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package jointhread;
public class Run {
public static void main(String[] args) {
try {
ThreadB b = new ThreadB();
ThreadA a = new ThreadA(b);
a.start();
b.start();
b.join(2000);
System.out.println("main ends at " + System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果1:
A begins at 1486956475786
A ends at 1486956480825
main ends at 1486956480857
B begins at 1486956480872
B ends at 1486956485880
step1. b.join(2000)先抢到B锁,然后将B锁释放;
step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);
step3. ThreadA打印 A ends,并释放锁;
step4. 此时join(2000)和ThreadB争抢锁,而join(2000)再次抢到锁,发现时间已过,释放锁后打印main ends;
step5. ThreadB抢到锁打印B begins;
step6. 5秒之后再打印B ends。
运行结果2:
A begins at 1486956475786
A ends at 1486956480825
B begins at 1486956480872
B ends at 1486956485880
main ends at 1486956485880
step1. b.join(2000)先抢到B锁,然后将B锁释放;
step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);
step3. ThreadA打印 A ends,并释放锁;
step4. 此时join(2000)和ThreadB争抢锁,而ThreadB抢到锁后执行sleep(5000)后释放锁;
step5. main ends 在最后输出。
运行结果3:
A begins at 1486956475786
A ends at 1486956480825
B begins at 1486956480872
main ends at 1486956480872
B ends at 1486956485880
step1. b.join(2000)先抢到B锁,然后将B锁释放;
step2. ThreadA抢到B锁,打印 A begins 并且sleep(5000);
step3. ThreadA打印 A ends,并释放锁;
step4. 此时join(2000)和ThreadB争抢锁,而join(2000)再次抢到锁,发现时间已过,释放锁后打印main ends;
step5. ThreadB抢到锁打印B begins;
step6. 这时main ends 也异步输出;
step7. 打印B ends。
6.类ThreadLocal
变量值的共享:public static +变量名;
每一个线程都有自己的共享变量:采用类ThreadLocal,使每一个线程绑定自己的值,保证隔离性。
package localthread;
public class Tools {
public static ThreadLocalExt t1 = new ThreadLocalExt();}
package localthread;
import java.util.Date;
public class ThreadLocalExt extends ThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
}
package localthread;
public class ThreadA extends Thread{
@Override
public void run() {
try {
for(int i = 0;i<10;i++){
System.out.println("ThreadA = " + Tools.t1.get());
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package localthread;
public class Run {
public static void main(String[] args) {
try {
for(int i = 0;i<10;i++){
System.out.println("main =" + Tools.t1.get());
Thread.sleep(100);
}
Thread.sleep(5000);
ThreadA a = new ThreadA();
a.start();
} catch (InterruptedException e) {
// TODO: handle exception
}
}
}
运行结果为10个main打印的值相等,10个ThreadA 打印的值也相等,但main和ThreadA 的不相等。
说明ThreadLocal能够实现一个线程享有一个专属变量。
7.类InheritableThreadLocal
可以在子线程中取得父线程继承下来的值。
(1) 将上例中Tools.java的ThreadLocal改为InheritableThreadLocal,即可得到ThreadA 打印的值和main一样,可谓继承性。
(2) InheritableThreadLocal.childValue(Object object) : 可以修改子线程中的值。
(3) 如果子线程取得值的同时,主线程将InheritableThreadLocal中的值进行修改,子线程拿到的值还是旧值。
三. 线程组
1.作用:可以批量管理线程或线程组对象。
Thread aRunnable = new ThreadA();
ThreadGroup group = new ThreadGroup("小胖的线程组");
Thread aThread = new Thread(group, aRunnable);
2.在实例化一个线程组如果不指定所属的线程组,则该线程组自动归到当前线程对象所属的线程组中,也就是隐式地在一个线程组中添加了一个子线程组。
JVM的根线程组是system,再取其父线程组则出现异常。
3.递归与非递归取得组内对象。
enumerate(ThreadGroup, true);//递归(打印A->B)
enumerate(ThreadGroup, false);//非递归(只打印A)
4.在默认的情况下,线程组中的一个线程出错,不影响其他线程的运行。