多线程基础
什么是多线程?
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
为什么要用多线程?
- 充分利用多处理核心
- 更快的响应时间
java默认的线程
public static void main(String[] args) { // java虚拟机的线程管理接口 ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 获取线程信息的方法 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false); for (ThreadInfo threadInfo:threadInfos) { System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName()); }
}
输出:
11:Monitor Ctrl-Break //监听中断信号 5:Attach Listener //获取内存dump,线程dump 4:Signal Dispatcher //将信号分给jvm的线程 3:Finalizer //调用对象的finalizer 方法 2:Reference Handler //清除Reference 1:main //主线程
其中还有一个GC垃圾回收的线程,但是由于没有产生垃圾堆栈,所以该线程没有启动。
线程的状态
一共包括四种状态
- 新创建(NEW) 线程被创建,但是没有调用start方法
- 可运行(RUNNABLE) 运行状态,有CPU决定是否正在运行
- 被阻塞(BLOCKING) 线程能够运行,但有某个条件组织它的运行。进入阻塞的原因:
- 通过调用sleep()使任务进入休眠状态。
- 通过调用wait()使线程挂起,知道线程得到了notify()或notifyAll()消息。
- 任务在等待某个输入/输出完成。
- 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用。
- 死亡(DEAD) 处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
启动和退出
创建线程有两种方式,一种是继承Thread类,还有一种就是实现Runnable接口。
public class StartThread {
public static class ThreadTest extends Thread { @Override public void run() { System.out.println("Thread id = " + Thread.currentThread().getId() + " ThreadTest running"); } } public static class RunnableTest implements Runnable { @Override public void run() { System.out.println("Thread id = " + Thread.currentThread().getId() + " RunnableTest running"); } } public static void main(String[] args) { ThreadTest thread = new ThreadTest(); Thread runnable = new Thread(new RunnableTest()); thread.start(); runnable.start(); } }
线程的退出有两种情况:
- run()方法执行完毕正常退出。
- 抛出一个未处理的异常导致线程的提前结束。
这里run()方法只是普通方法,只用通过start()方法执行后才是线程执行。
取消和中断
不安全的中断
- 只使用一个取消标志位。
- 使用stop()/suspend()/resume()方法,这些方法是过期的API,有很大副作用,很容易导致死锁和数据不一致。
安全的中断
使用下面三个方法来实现安全中断:
- interrupt()方法中断线程,本质是将线程的中断位设为true,是否真正中断由线程决定。
- isInterrupt()方法检查线程自己的中断位标志。
- Thread.interrupted()静态方法将中断位设置为false。
由于java线程管理机制为协作式任务分配,而不是抢占式的,当线程处于阻塞(包括sleep、wait等方法)的时候,线程是不会理会自定义的标志位,但阻塞线程会检查线程的标志位。
处理不可中断的阻塞
当IO通信InputStream 的read/write等阻塞方法,是不会处理中断的,而需要在中断的基础上关闭套接字socket.close()会抛出SocketException。
NIO时的selector.select()会阻塞,调用selector的wakeup和close方法会抛出ClosedSelectorException。
死锁状态不响应中断请求,只能重启修改错误。
重写interrupt方法中断IO通信
public class OverrideInterrupt extends Thread { private final Socket socket; private final InputStream in; public OverrideInterrupt(Socket socket, InputStream in) { this.socket = socket; this.in = in; } @Override public void interrupt() { try { //关闭底层的套接字 socket.close(); } catch (IOException e) { e.printStackTrace(); //..... }finally { //同时中断线程 super.interrupt(); } } }
线程的优先级
priority字段控制优先级,范围1~10之间,数字越高优先级越高,缺省是5,创建线程时setPriority()方法可以设置,但是不一定起作用,Ubuntu和Max OS设置就无效。
守护线程
守护线程是指程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中所有的守护线程。
public class SimpleDaemons implements Runnable { public void run() { try { while(true) { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } } catch(InterruptedException e) { System.out.println("sleep() interrupted"); } finally { System.out.println(Thread.currentThread() + " finally"); } } public static void main(String[] args) throws Exception { for(int i = 0; i < 5; i++) { Thread daemon = new Thread(new SimpleDaemons()); daemon.setDaemon(true); // 设置为守护线程 daemon.start(); } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(175); } }
输出:
All daemons started Thread[Thread-2,5,main] org.eocencle.concurrency.SimpleDaemons@3904dd9e Thread[Thread-3,5,main] org.eocencle.concurrency.SimpleDaemons@33108c41 Thread[Thread-0,5,main] org.eocencle.concurrency.SimpleDaemons@38aca878 Thread[Thread-4,5,main] org.eocencle.concurrency.SimpleDaemons@680d099e Thread[Thread-1,5,main] org.eocencle.concurrency.SimpleDaemons@27ae93f
这里可以看出并没用输出finally,这是因为主线程执行完毕之后关闭了JVM,所以守护线程是执行不到finally的。
常用方法
- sleep()方法,让线程休息多少毫秒(ms),该方法不会释放锁,所以使用sleep时,需要把他放在同步代码块外面。
- yield()方法,当前线程出让CPU占有权,当前线程变为可运行状态下,下一刻可能仍然被CPU选中,不会释放锁。
- wait()方法,让当前线程等待,并释放锁。
- notify()方法,唤醒当前锁对象等待的一个线程,如果有多个则随机唤醒一个。
- notifyAll()方法,唤醒当前锁对象等待的所有线程。
notify()和notifyAll()这两个方法,不管在同步代码块的哪部分,唤醒线程之前也会把当下同步代码块执行完。
线程间协作和通信
volatile和synchronized
多线程同时访问一个共享变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存中。
volatile修饰字段,对这个变量的访问必须从共享内存刷新一次,最新的修改写回共享内存。可以保证字段的可见性,但绝不是线程安全,因为没有操作的原子性。
使用场景:
- 一个线程写,多个线程读
- volatile变量的变化很固定
synchronized可以修饰方法或者代码块,主要保证多个线程在同一时刻,只能有一个线程处于方法或代码块中,他保证了线程对变量访问的可见性和排他性,又称内置锁机制。
synchronized分为类锁和对象锁,前者锁class这个文件,后者锁类的实例,当出现两种锁时,是不会相互干扰的。
等待和通知机制
等待方原则
- 获取对象锁
- 如果条件不满足,调用对象的wait()方法,被通知后依然要检查条件是否满足
- 条件满足以后,才能执行相关的业务逻辑
synchronized(对象) { while(条件不满足) { 对象.wait(); } // 业务逻辑处理 }
通知方原则
- 获得对象的锁
- 改变条件
- 通知所有等待对象的线程
synchronized(对象){ // 业务逻辑处理,改变条件 对象.notify()/notifyAll(); }
注:这里业务逻辑写在通知之前和之后没有关系,因为通知也得需要把当前代码块执行完。
实现阻塞队列
public class BlockingQueueWN<T> { private List<T> queue = new LinkedList<>(); private int limit = 0; public BlockingQueueWN(int limit) { super(); this.limit = limit; } public void push(T el) throws InterruptedException { synchronized (this) { while (this.limit == this.queue.size()) { this.wait(); } this.queue.add(el); this.notifyAll(); } } public T pull() throws InterruptedException { synchronized (this) { while (0 == this.queue.size()) { this.wait(); } this.notifyAll(); return this.queue.get(0); } } }
本文索引关键字:
volatile:http://www.cnblogs.com/huanStephen/p/8157431.html#volatile
synchronized/内置锁机制:http://www.cnblogs.com/huanStephen/p/8157431.html#synchronized
欢迎大家索引!