什么是线程
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程的多个线程共享进程的资源。
java启动main函数其实就是启动了一个JVM的进程,而main函数所在的线程就是这个进程的一个线程,也称主线程。
进程和线程关系
一个进程有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行java代码时pc计数器记录的才是下一条指令的地址;
进程的栈资源存储该线程的局部变量,局部变量是该线程私有的,其它线程访问不了,除此之外栈还用来存放线程的调用栈帧;
堆是进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面存放使用new操作创建的对象实例。
方法区则用来存放JVM加载的类,常量及静态变量等信息,也是线程共享的。
线程创建和运行
创建方式:继承Thread类 实现Runnable接口,使用FutureTask方式
1.继承Thread类
public class ThreadTest { public static class MyThread extends Thread{ @Override public void run(){ System.out.println(" I am a child thread!") ; } public static void main(String[] args){ MyThread t = new MyThread(); t.start(); } } }
2.实现Runnable接口
public class RunnableTest { public static class MyRunnable implements Runnable { @Override public void run() { System.out.println("I am a child thread!"); } } public static void main(String[] args) { new Thread(new MyRunnable()).start(); } }
3.使用FutureTask
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTest implements Callable<Boolean>{ @Override public Boolean call() throws Exception { return true; } public static void main(String[] args) { FutureTask<Boolean> f = new FutureTask<>(new CallableTest()); new Thread(f).start(); try{ Boolean r = f.get(); System.out.println(r); }catch(Exception e) { e.printStackTrace(); } } }
小结:
使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者构造函数传递参数,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread,那么子类不能再继承其它类,而Runnale则没有这个限制。前两种方式都没办法拿到任务的返回值,但是Futuretask方式可以。
线程通知与等待
java中的Object类是所有类的父类,鉴于继承机制,java把所有类都需要的方法放到了Object类里面,其中就包含通知与等待系列函数。
wait()函数
当一个函数调用共享变量的wait()方法时,该线程会被阻塞挂起,直到发生下面几件事情之一才返回:1)其它线程调用了该共享对象的notify()或者notifyAll()方法;2)其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回;
如果调用wait()方法的线程事先没有获取该对象的监视器锁,会抛出IllegalMonitorStateException异常;
获取共享变量的监视器锁
synchronized(共享变量){doSomething}
synchronized void add(int a,int b){doSomething}
虚假唤醒
一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即时该线程没有被其它线程调用notify(),notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
生产者和消费者
/** * 容器 * @author pc * */ public class Box { public final int MAX_SIZE = 10; public LinkedList<Integer> box = new LinkedList<>(); public void add() throws Exception { while(true) { synchronized(box) { while(box.size() == MAX_SIZE) { box.wait(); } box.push(1); System.out.println("add ele and notifyall:"+box.size()); box.notifyAll(); } } } public void remove() throws Exception { while(true) { synchronized(box) { while(box.size() == 0) { box.wait(); } box.pop(); System.out.println("remove ele and notifyall:"+box.size()); box.notifyAll(); } } } } /** * 生产者和消费者 * @author pc * */ public class Program { public Box box; public Program(Box box) { super(); this.box = box; } public class Consumer implements Runnable { @Override public void run() { try { box.remove(); } catch (Exception e) { e.printStackTrace(); } } } public class Producer implements Runnable { @Override public void run() { try { box.add(); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { Program p = new Program(new Box()); Program.Consumer c = p.new Consumer(); Program.Producer pd = p.new Producer(); Thread t1 = new Thread(c); Thread t2 = new Thread(pd); t1.start(); t2.start(); } }
当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其它共享变量的锁,则这些锁时不会被释放的。
public class Test { private static volatile Object resourceA = new Object(); private static volatile Object resourceB = new Object(); public static void main(String[] args) throws Exception { Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("t1 try get resourceA lock..."); synchronized(resourceA) { System.out.println("t1 get resourceA lock..."); System.out.println("t1 try get resourceB lock..."); synchronized(resourceB) { System.out.println("t1 get resourceB lock..."); System.out.println("t1 unlock resourceA..."); try { resourceA.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000L);//等待1s保证t1将resourceA释放出来 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 try get resourceA lock..."); synchronized(resourceA) { System.out.println("t2 get resourceA lock..."); System.out.println("t2 try get resourceB lock..."); synchronized(resourceB) { System.out.println("t2 get resourceB lock..."); System.out.println("t2 unlock resourceA..."); try { resourceA.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("main thread is over"); } }
当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其它线程中断了该线程,则该线程会抛出InterruptedException异常并返回
public class InterruptedExceptionTest { private static volatile Object resouce = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized(resouce) { try { resouce.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t1.start(); System.out.println(" start interupt=="); t1.interrupt(); System.out.println(" end interupt=="); System.out.println("main is over"); } }
wait(long timeout)函数
该方法相比wait()函数多了一个超时参数,如果线程调用共享变量过程中,没有在指定的timeout时间内被其它线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时返回
timeout等于0就相当于wait()函数
timeout小于0会报错IllegalArgumentException异常
public class WaitTimeOutTest { private static Object resource = new Object(); public static void main(String[] args)throws Exception { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized(resource) { System.out.println("start wait()"+System.currentTimeMillis()/1000); try { resource.wait(1000L); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("end wait()"+System.currentTimeMillis()/1000); } } }); t1.start(); t1.join(); }
wait(long timeout,int nanos)
内部调用的时wait(long timeout),只有在nanos>0时才使参数timeout递增1;
notify()函数
一个线程调用共享变量的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程时随机的。
被唤醒的线程不会立即返回,需要先获取共享变量的锁。
类似wait系列方法,只有当前线程获取到共享变量的监视器锁之后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。
notifyAll()函数
唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程
如果调用notifyAll()之后有线程调用了wait()方法,则该线程不能被唤醒。
public class NotifyTest { private static Object resource = new Object(); public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized(resource) { System.out.println("t1 start wait()"); try { resource.wait(); System.out.println("t1 end wait()"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized(resource) { System.out.println("t2 start wait()"); try { resource.wait(); System.out.println("t2 end wait()"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); }//等待1s synchronized(resource) { System.out.println("t3 start notify()"); resource.notify();
//resource.notifyAll(); } } }); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
等待线程执行终止的join方法
ublic class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 end"); } }); t1.start();t2.start(); System.out.println("wait t1,t2 over"); t1.join();t2.join(); } }
让线程睡眠的sleep方法
Thread类中的一个静态sleep方法,当一个执行中的线程调用Thread的sleep方法后,调用线程会暂时让出指定时间的使用权,也就是不参数与CPU调度,但是该线程拥有的监视器资源,比如锁还是持有不让出的。指定时间结束后该函数会正常返回。
如果在睡眠期间调用了该线程的interrupt()方法中断了该线程,则线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SleepTest { private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { try { lock.lock(); System.out.println("t1 start sleep"); Thread.sleep(1000); System.out.println("t1 end sleep"); }catch(Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { lock.lock(); System.out.println("t2 start sleep"); Thread.sleep(1000); System.out.println("t2 end sleep"); }catch(Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }); t1.start(); t2.start(); t1.join(); t2.join(); } }
输出结果一直是t1或t2结束其中之一结束之后才开始执行下一线程,证明线程睡眠期间不会释放锁;
让出CPU执行权的yield方法
Thread类中的静态yield方法,当一个线程调用yield方法时,其实就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级高的线程,当然也有可能调度到刚刚让出CPU的线程;
线程中断
java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理
void interrupt():中断线程
boolean isInterrupted():检测当前线程是否被中断
boolean interrupted():检测当前线程是否被中断,中断返回true,并清除中断标志
线程死锁
死锁是指两个或两个以上的线程执行过程中,因争夺资源而做成的互相等待的现象。无外力作用的情况下,这些线程会一直互相等待下去而无法继续运行下去
死锁出现的条件
1)互斥条件:线程堆已经获取的资源进行排他性使用,即该资源同时只能有一条线程占用。如果此时还有其它线程请求获取该资源,请求者只能等待,直至占有的线程释放该资源;
2)请求并持有条件:指一个线程已经持有了至少一个资源,但是又提出了新的资源请求,而新资源被其它线程占有,所以当前线程会被阻塞,但阻塞的同事并不释放自己已经获取的资源。
3)不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放资源
4)环路等待条件:发生死锁时,必然存在一个线程-资源环形链,即线程集合{T0,T1,T2,...Tn}中T0等待T1的资源,T1等待T2的资源,...Tn正在等待T0占用的资源
public class DeadThreadTest { private volatile static Object resourceA = new Object(); private volatile static Object resourceB = new Object(); public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("t1 try get resourceA lock..."); synchronized(resourceA) { System.out.println("t1 get resourceA lock..."); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 try get resourceB lock..."); synchronized(resourceB) { System.out.println("t1 get resourceB lock..."); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("t2 try get resourceB lock..."); synchronized(resourceB) { System.out.println("t2 get resourceB lock..."); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 try get resourceA lock..."); synchronized(resourceA) { System.out.println("t2 get resourceA lock..."); } } } }); t1.start(); t2.start(); } }
输出:
t2 try get resourceB lock...
t1 try get resourceA lock...
t1 get resourceA lock...
t2 get resourceB lock...
t1 try get resourceB lock...
t2 try get resourceA lock...
如何避免死锁的发生
1)破坏掉至少一个构造死锁的必要条件即可
2)使用资源的有序性原则
守护线程与用户线程
线程分类:守护线程【daemon】,用户线程【user】
main函数就是用户线程
当最后一个用户线程结束时,JVM会正常退出,只要有一个用户线程没有结束,正常情况下JVM就不会退出
public class DamonThreadTest { public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for(;;) { } } }); t1.setDaemon(false); t1.start(); System.out.println("main is over"); }
ThreadLocal
多线程访问同一共享变量时特别容易出现并发问题,特别时在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步
同步措施一般是加锁,这就需要使用者对锁有一定的了解
ThreadLocal提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。
public class ThreadLocalTest { private static ThreadLocal<String> local = new ThreadLocal<>(); public static void print(String name) { System.out.println(name+"--"+local.get()); } public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { local.set("1234"); print(Thread.currentThread().getName()); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { local.set("5678"); print(Thread.currentThread().getName()); } },"t2"); t1.start(); t2.start(); } }
output:
t1--1234
t2--5678
线程上下文切换
多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程在同时运行,CPU采用采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务,
当前线程使用完时间片后,就会处于就绪状态并让出CPU让其它线程占用,这就是上下文切换。
切换时机:1)当前线程使用完CPU分配的时间片处于就绪状态 2)当前线程被其它线程中断