JAVA高并发学习笔记(二) 多线程基础
2015-09-07 18:28
976 查看
1.1什么是线程
线程是进程(程序在计算机上的一次执行活动)内的执行单元进程是以独立于其他进程的方式运行的,进程间是互相隔离的。一个进程无法直接访问另一个进程的数据。进程的资源诸如内存和CPU时间片都是由操作系统来分配。
线程又被称为轻量级进程。每个线程有它独自的调用栈, 但是在同一进程下的线程又能互相访问它们间的共享数据。每个线程都有它独自的缓存。如果一个线程读取了某些共享数据,那么它将这些数据存放在自己的缓存中以供将来再次读取。
虽然进程和线程的上下文切换的处理都是通过操作系统内核来完成的(内核的这种切换过程最显著的性能损耗就是内容切出) ,一旦去切换上下文,处理器中缓存的内存地址瞬间作废。当改变虚拟内存空间的时候,处理的页表缓存或者相当的神马东西会刷新TLB,导致内存的访问在一段时间内非常的低效,因为线程的切换虚拟内存空间依然是相同的,而且现在的cpu特地增加了线程寄存器,大大加快了线程的切换。
线程的基本操作
1.2.1 新建
新建的一个线程处于 NEW 状态,没有开始工作,只是静态的一个实体1.2.2 启动
调用start 方法,线程被启动,进入RUNNABLE 状态(线程可以执行,一切准备就绪),如果CPU没有办法分出时间片来执行,这个时候也是不能执行的,具体的执行需要物理CPU的调度,1.2.3 结束
如果一个线程所有的工作都做完了,那么就自然被终结,进入TERMINATED状态,表示线程任务执行结束1.2.4 线程运行期间
在线程执行的过程,不可避免的申请某些锁,如:申请某个对象的监视器,调用了synchronized 的方法,这个时候线程有可能会被阻塞住(BLOCKED),这个线程会被挂起,因为这个线程在进入临界区之前要申请到对象的一把锁,这个锁可能被其他的线程占用,没有办法拿到,导致线程被挂起。如果一个线程在执行过程中,调用了WAITING方法,那么这个线程就会进入wait(等待)状态,进入等待状态的线程会等候另外线程的notify(通知),被通知到后,从等待状态进入RUNNABLE状态。
等待状态有2钟。
① WAITING 无限期等待,等待notify
② TIME_WAITING 等待指定的时间间隔,如果没有通知,就自己notify
1.2.5 新建线程
Thread t1 = new Thread(); t1.start();//开启新线程,并启动 Thread t2 = new Thread(); //不能开启新的线程,在执行该方法的线程上启动 t2.run();
调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。
当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。当线程终止之后,是不能被重新启动的。在同一个Thread上调用多次start方法会抛出InvalidThreadStateException异常。
如果线程已经启动但是还没有终止,那么调用isAlive方法就会返回true。即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回true。如果线程已经被取消(cancelled),那么调用其isAlive在什么时候返回false就因各Java虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过了(即线程在开始运行前和结束运行后都会返回false,你无法得知处于false的线程具体的状态)。另一点,虽然一个线程能够得知同一个线程组的其他线程的标识,但是却无法得知自己是由哪个线程调用启动的。
Thread实现了Runnable接口并重写了run() 方法;
新建线程的时候重写Thread的run() 方法
新建线程的时候通过构造方法传入一个实现Runnable接口的实例
1.2.6 终止线程
如果2个线程同时操作一个对象数据(ID,NAME),防止对象被多个线程同时修改,加上锁,这时线程1获取到记录1(ID=1,NAME=小明),在写入ID=1之后准备写入NAME = 小明,线程1被stop()掉了,线程1释放锁。线程2一直在等待发现线程1释放,获取对象数据(ID=1,NAME=NULL),从而导致数据不一致。
1.2.7 线程中断
每个线程都有一个相关的Boolean类型的中断标识。在线程t上调用t.interrupt会将该线程的中断标识设为true。任何一个线程的中断状态都可以通过调用isInterrupted方法来得到。如果线程已经通过interrupt方法被中断,这个方法将会返回true。
/** * 线程死循环一直在执行 */ public void run(){ while (true) { do something ....; } } //通知线程需要中断 t1.interrupt();
上述代码,虽然t1执行了interrupt()方法,通知了线程需要进行中断,但是线程没有在中断状态下做处理操作,所以线程会一直死循环执行下去。
/** * 线程死循环一直在执行 */ public void run() { while (true) { //判断线程状态是否被中断 if (Thread.currentThread().isInterrupted()) { System.out.println("线程被中断"); //跳出while死循环 break; } do something ....; } } //通知线程需要中断 t1.interrupt();
上述代码,t1通知线程需要中断,线程在执行的时候,每次循环开始都会判断线程状态是否处于中断状态,如果是处于中断,则跳出。
大部分需要线程处于等待状态的方法都会抛出一个InterruptedException 异常,因为如果线程执行interrupt()方法通知线程中断,这些等待是毫无意义的,并且线程应该对通知做出一个响应,所以处于等待状态的时候有线程通知进入中断状态,线程需要立即做出反馈,这时候就会抛出InterruptedException 异常并进入catch代码块里面,这个时候就能做出某些动作,比如结束。
线程t正处于Object.wait,Thread.sleep,或者Thread.join,这些情况下interrupt调用会导致t上的这些操作抛出InterruptedException异常,但是t的中断标识会被设为false。
/** * 线程死循环一直在执行 */ public void run() { while (true) { //判断线程状态是否被中断 if (Thread.currentThread().isInterrupted()) { System.out.println("线程被中断"); //跳出while死循环 break; } try { Thread.sleep(1000); //线程在睡眠状态下被中断,进入catch代码块 } catch (InterruptedException e) { System.out.println("Interrupted When Sleep"); //因为在抛出异常之后,会清除中断标记,所以要重新中断线程 Thread.currentThread().interrupt(); do something ....; } do something ....; } }
1.2.8 挂起(suspend)和继续执行(resume)
如果目标线程获得了一个锁,被suspend了,目标线程并不会释放锁,因此其他线程都不能访问临界区资源,直到目标线程被调用了resume方法
如果resume方法先于suspend方法调用,使线程没有办法在执行下去,类似于线程被冻结的状态。造成死锁
public class BadSuspend { public static Object u = new Object(); static ChangeObjectThread t1 = new ChangeObjectThread("t1"); static ChangeObjectThread t2 = new ChangeObjectThread("t2"); public static class ChangeObjectThread extends Thread { public ChangeObjectThread(String name) { super.setName(name); } @Override public void run() { synchronized (u) { System.out.println("in "+getName()); //进行挂起操作,挂起操作后导致 U 无法被释放 Thread.currentThread().suspend(); } } } public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(100); t2.start(); t1.resume(); t2.resume(); //在主线程等待t1 和t2结束 System.out.println("等待t1 和 t2 结束"); t1.join(); t2.join(); } }
执行结果:
通过jstack 通过 查看线程状态
上面没有t1线程,说明t1先调用了suspend()方法,然后调用了resume()方法。线程执行结束。发现t2的状态是RUNNABLE,t2卡在suspend0等待。所以t2在执行suspend()方法之前,先执行resume()方法。导致t2被冻结在哪里,无法继续执行下去。
1.2.9 等待线程结束(join)和谦让(yeild)
希望给予其他线程有机会来争夺CPU,会把自己占有的CPU释放掉,可以其他的线程有更多的机会继续往下正常的执行。虽然把CPU释放掉了,但是下一次还是会继续竞争CPU。可能会在dubug或者测试的时候使用。
在支持可抢占式调度的Java虚拟机实现上,线程调度器忽略yield操作可能是最完美的策略,特别是在多核处理器上。
使用多线程的时候不知道其中的线程会在什么时候执行结束,有时候需线程执行结束在去做下一件事情,所以希望等到线程结束,然后一直在执行。
比如2个人,在2条路上走,现在需要合伙一起走了,所以要在前面等待后方的人,然后一直走。
public class JoinMain { public volatile static int i = 0; public static class AddThread extends Thread { @Override public void run() { for (i = 0; i < 10000000; i++) ; } } public static void main(String[] args) throws InterruptedException { AddThread add = new AddThread(); add.start(); //等待add线程执行结束 System.out.println("等待线程执行结束!"); add.join(); System.out.println(i); } }
执行结果:
join的本质
1.3 守护线程
① 在后台默默地完成一些系统性的服务,比如垃圾回收线程、 JIT线程就可以理解为守护线程② 当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出
public class DaemonDemo { public static class DeamonT extends Thread { public void run() { while (true) { System.out.println("I am alive"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t = new DeamonT(); //把线程设置为守护线程,守护线程需要在start()方法之前设置 t.setDaemon(true); t.start(); Thread.sleep(2000); System.out.println("线程执行结束"); } }
执行结果:
public class DaemonDemo { public static class DeamonT extends Thread { public void run() { while (true) { System.out.println("I am alive"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t = new DeamonT(); //把线程设置为守护线程,守护线程需要在start()方法之前设置 // t.setDaemon(true); t.start(); Thread.sleep(2000); System.out.println("线程执行结束"); } }
执行结果:
public class DaemonDemo { public static class DeamonT extends Thread { public void run() { while (true) { System.out.println("I am alive"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t = new DeamonT(); //把线程设置为守护线程,守护线程需要在start()方法之前设置 t.start(); t.setDaemon(true); Thread.sleep(2000); System.out.println("线程执行结束"); } }
执行结果:
1.4 线程优先级
public final static int MIN_PRIORITY = 1;public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)
默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).
线程的当前优先级可以通过getPriority方法获得。
线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。
当可运行的线程数超过了可用的CPU数目的时候,线程调度器更偏向于去执行那些拥有更高优先级的线程。具体的策略因平台而异。比如有些Java虚拟机实现总是选择当前优先级最高的线程执行。有些虚拟机实现将Java中的十个优先级映射到系统所支持的更小范围的优先级上,因此,拥有不同优先级的线程可能最终被同等对待。还有些虚拟机会使用老化策略(随着时间的增长,线程的优先级逐渐升高)动态调整线程优先级,另一些虚拟机实现的调度策略会确保低优先级的线程最终还是能够有机会运行。设置线程优先级可以影响在同一台机器上运行的程序之间的调度结果,但是这不是必须的。
优先级仅仅是用来表明哪些线程是重要紧急的,当存在很多线程在激烈进行CPU资源竞争的情况下,线程的优先级标识将会显得非常有用。
优先级范围:
范围 | 用途 |
10 | Crisis management(应急处理) |
7-9 | Interactive, event-driven(交互相关,事件驱动) |
4-6 | IO-bound(IO限制类) |
2-3 | Background computation(后台计算) |
1 | Run only if nothing else can(仅在没有任何线程运行时运行的) |
public class PriorityDemo { public static class HightPriority extends Thread { static int count = 0; public void run() { while (true) { //执行上锁,如果有2个线程一直执行,会产生数据竞争 synchronized (PriorityDemo.class) { count ++; if (count > 100000000) { System.out.println("HightPriority is complete"); break; } } } } } public static class LowPriority extends Thread { static int count = 0; public void run() { while (true) { //执行上锁,如果有2个线程一直执行,会产生数据竞争 synchronized (PriorityDemo.class) { count ++; if (count > 100000000) { System.out.println("LowPriority is complete"); break; } } } } } public static void main(String[] args) throws InterruptedException { //高优先级不能保证在任何场合一定能抢到资源,只是高优先级有更高的概率抢占到系统资源 HightPriority high = new HightPriority(); LowPriority low = new LowPriority(); //这是优先级 MAX_PRIORITY最高级优先级 high.setPriority(Thread.MAX_PRIORITY); //这是优先级 MIN_PRIORITY最低级优先级 low.setPriority(Thread.MIN_PRIORITY); low.start(); high.start(); } }
执行结果:
1.5 基本的线程同步操作
在多线程执行过程之间,多线程如果进行通讯1.如果线程被挂起、被等待,别的怎么唤醒该线程,通知线程怎么继续执行?
2.如果线程之间有数据竞争,那么彼此之间怎么协调竞争?
synchronized:
指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
synchronized关键字并不是方法签名的一部分。所以当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,另外,构造方法不可能是真正同步的(尽管可以在构造方法中使用同步块)。
使用synchronized关键字须遵循一套内置的锁等待-释放机制。所有的锁都是块结构的。当进入一个同步方法或同步块的时候必须获得该锁,而退出的时候(即使是异常退出)必须释放这个锁。你不能忘记释放锁。
同步方法或同步块遵循这种锁获取/锁释放的机制有一个前提,那就是所有的同步方法或同步块都是在同一个锁对象上。如果一个同步方法正在执行中,其他的非同步方法也可以在任何时候执行。也就是说,同步不等于原子性,但是同步机制可以用来实现原子性。
给指定对象加锁:
public class AccountingSync implements Runnable { static AccountingSync instance = new AccountingSync(); static int i = 0; @Override public void run() { for (int j = 0; j < 100000000; j++) { //指定对象加锁 synchronized (instance) { i ++ ; } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); //等待t1,t2执行完毕 t1.join(); t2.join(); //如果线程是安全的输出的结果必然是200000000,不安全的一定比200000000小 System.out.println(i); } }
运行结果:
public class AccountingSync implements Runnable { static AccountingSync instance = new AccountingSync(); static int i = 0; @Override public void run() { for (int j = 0; j < 100000000; j++) { // synchronized (instance) { i ++ ; // } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); //等待t1,t2执行完毕 t1.join(); t2.join(); //如果线程是安全的输出的结果必然是200000000,不安全的一定比200000000小 System.out.println(i); } }
运行结果:
给指定对象实例加锁:
public class AccountingSync2 implements Runnable { static AccountingSync2 instance = new AccountingSync2(); static int i = 0; public synchronized void increase() { i++ ; } @Override public void run() { for (int j = 0; j < 100000000; j++) { increase() ; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); //如果线程是安全的输出的结果必然是200000000,不安全的一定比200000000小 System.out.println(i); } }
输出结果:
注意:
切勿将锁加错对象实例
public class AccountingSyncBad implements Runnable{ static int i = 0; public synchronized void increase() { i++ ; } @Override public void run() { for (int j = 0; j < 100000000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { //下面t1和t2 都是自己new 出来了一个 AccountingSyncBad对象,不是同一个对象实例 Thread t1 = new Thread(new AccountingSyncBad()); Thread t2 = new Thread(new AccountingSyncBad()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
输出结果:
给指定类加锁:
public class AccountingSyncClass implements Runnable { static int i = 0; public static synchronized void increase() { i++ ; } @Override public void run() { for (int j = 0; j < 100000000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { //因为静态方法是给指定类加锁,所以线程是安全的 Thread t1 = new Thread(new AccountingSyncClass()); Thread t2 = new Thread(new AccountingSyncClass()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
运行结果:
正如每个对象都有一个锁一样,每一个对象同时拥有一个由这些方法(wait,notify,notifyAll,Thread,interrupt)管理的一个等待集合。拥有锁和等待集合的实体通常被称为监视器(monitor,虽然每种语言定义的细节略有不同),任何一个对象都可以作为一个监视器。
因为与等待集合交互的方法(wait,notify,notifyAll)只在拥有目标对象的锁的情况下才被调用,因此无法在编译阶段验证其正确性,但在运行阶段错误的操作会导致抛出IllegalMonitorStateException异常。
Object.wait()
线程等待在当前对象上
调用wait方法会产生如下操作:
① 如果当前线程已经终止,那么这个方法会立即退出并抛出一个InterruptedException异常。否则当前线程就进入阻塞状态。
② Java虚拟机将该线程放置在目标对象的等待集合中。
③ 释放目标对象的同步锁,但是除此之外的其他锁依然由该线程持有。即使是在目标对象上多次嵌套的同步调用,所持有的可重入锁也会完整的释放。这样,后面恢复的时候,当前的锁状态能够完全地恢复。
Object.notity()
通知等待在当前对象上的线程从wait状态返回
调用Notify会产生如下操作:
① Java虚拟机从目标对象的等待集合中随意选择一个线程(称为T,前提是等待集合中还存在一个或多个线程)并从等待集合中移出T。当等待集合中存在多个线程时,并没有机制保证哪个线程会被选择到。
② 线程T必须重新获得目标对象的锁,直到有线程调用notify释放该锁,否则线程会一直阻塞下去。如果其他线程先一步获得了该锁,那么线程T将继续进入阻塞状态。
③ 线程T从之前wait的点开始继续执行。
public class SimpleWN { final static Object object = new Object() ; static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class T1 extends Thread { @Override public void run() { synchronized (object) { System.out.println(sdf.format(System.currentTimeMillis())+":T1 start! "); try { System.out.println(sdf.format(System.currentTimeMillis())+":T1 wait for object"); //需要拿到object对象的监视器之后才能执行wait(0方法 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } //如果另外有对象获取到object对象的监视器并执行了notify()方法,等待线程执行结束,该线程在获取到对象后再继续执行 System.out.println(sdf.format(System.currentTimeMillis())+":T1 end"); } } } public static class T2 extends Thread { @Override public void run() { synchronized (object) { System.out.println(sdf.format(System.currentTimeMillis())+ ":T2 start! notify one thread"); //需要拿到object对象的监视器之后才能执行notify(); object.notify(); System.out.println(sdf.format(System.currentTimeMillis())+ ":T2 end!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new T1(); Thread t2 = new T2(); t1.start(); t2.start(); t1.join(); t2.join(); } }
执行结果:
notifyAll:
public class SimpleWN { final static Object object = new Object() ; static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class T1 extends Thread { public T1(String name) { super.setName(name); } @Override public void run() { synchronized (object) { System.out.println(sdf.format(System.currentTimeMillis())+":"+getName()+" start! "); try { System.out.println(sdf.format(System.currentTimeMillis())+":"+getName()+" wait for object"); //需要拿到object对象的监视器之后才能执行wait(0方法 object.wait(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //如果另外有对象获取到object对象的监视器并执行了notify()方法,等待线程执行结束,该线程在获取到对象后再继续执行 System.out.println(sdf.format(System.currentTimeMillis())+":"+getName()+" end"); } } } public static class T2 extends Thread { public T2(String name) { super.setName(name); } @Override public void run() { synchronized (object) { System.out.println(sdf.format(System.currentTimeMillis())+ ":"+getName()+" start! notify all thread"); //需要拿到object对象的监视器之后才能执行notifyAll(); object.notifyAll(); System.out.println(sdf.format(System.currentTimeMillis())+ ":"+getName()+" end!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new T1("T1"); Thread t2 = new T1("T2"); Thread t3 = new T2("T3"); t1.start(); t2.start(); Thread.sleep(1000); t3.start(); t1.join(); t2.join(); t3.join(); } }
执行结果:
nterrupt(中断)
如果在一个因wait而中断的线程上调用Thread.interrupt方法,之后的处理机制和notify机制相同,只是在重新获取这个锁之后,该方法将会抛出一个InterruptedException异常并且线程的中断标识将被设为false。如果interrupt操作和一个notify操作在同一时间发生,那么不能保证那个操作先被执行,因此任何一个结果都是可能的。(JLS的未来版本可能会对这些操作结果提供确定性保证)
相关文章推荐
- C#线程间不能调用剪切板的解决方法
- C#线程同步的三类情景分析
- C#获取进程或线程相关信息的方法
- C#停止线程的方法
- C#子线程更新UI控件的方法实例总结
- C#线程队列用法实例分析
- C++使用CriticalSection实现线程同步实例
- 基于C++实现的线程休眠代码
- 内核线程优先级设置的方法介绍
- VB读取线程、句柄及写入内存的API代码实例
- C#网络编程基础之进程和线程详解
- C#通过Semaphore类控制线程队列的方法
- C#多线程处理多个队列数据的方法
- C#实现线程安全的简易日志记录方法
- C#中线程同步对象的方法分析
- ASP.NET线程相关配置
- 浅析linux环境下一个进程最多能有多少个线程
- 再谈JavaScript线程
- C#实现终止正在执行的线程
- 解析Java线程同步锁的选择方法