JAVA基础(未看)@IT·互联网

Java多线程编程核心技术1——Thread类核心API

2017-02-17  本文已影响237人  有奶喝先森

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.在默认的情况下,线程组中的一个线程出错,不影响其他线程的运行。

上一篇下一篇

猜你喜欢

热点阅读