线程
线程是操作系统的最小调度单元。操作系统在运行一个程序的时候会为其创建一个进程,如创建一个java程序操作系统就会创建一个进程,线程也叫做轻量级进程,在一个进程里面可以创建多个线程。这些线程拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量,处理器在这些线程之间高速切换使得使用者感觉这些线程在同时进行
线程优先级
现代操作及系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,时间片用完了就会发生县城调度,并等待下次分配。
线程分配到的时间骗多少也就决定了线城市用处理器资源的多少,县城优先级就是决定线程需要分配多少处理器资源的线程属性。
这个属性由一个整型成员变量priority来控制。
线程优先级不能作为程序正确性的依赖,因为有些操作系统会完全不用理会优先级的定义。
线程的状态
线程一共有6种状态,同一时间只能处于一种状态。
- NEW 初始状态,线程被构建但是还没有调用start()方法
- RUNNABLE 运行状态
- BLOCKED 阻塞状态
- WAITING 等待状态 等待其它线程
- TIME_WAITING 超时等待,可以再指定的时间自行返回。
- TEMINATED 终止状态
当线程调用同步方法的时候,没有获取锁时,将会进入阻塞状态。执行runnable的run()之后会进入终止状态。
线程的启动和终止
一个新构造的线程对象是由其parent线程来进行空间分配的。会分配一个唯一得ID来标识这个child线程。线程对象被初始化好了之后,在堆内存中等待运行。
启动线程
线程在初始化完成之后,调用start()方法就可以启动,这个方法的含义是:当前线程(parent)同步告知java虚拟机,只要线程规划器空闲,应该立即启用调用start()方法的线程
线程中断
中断好比其它线程对这个线程打了个招呼,其它线程通过调用该线程的interrupt()方法对其进中断操作。
过期的suspend(),resume(),stop()
package com.page94;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Deprecated {
public static void main(String[] args) throws Exception{
DateFormat format = new SimpleDateFormat("HH:mm:ss");
Thread printThread = new Thread(new Runner(),"PrintThread");
printThread.setDaemon(true);
printThread.start();
TimeUnit.SECONDS.sleep(3);
printThread.suspend();
System.out.println("main suspend PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.resume();
System.out.println("main resume PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
printThread.stop();
System.out.println("main stop PrintThread at" +format.format(new Date()));
TimeUnit.SECONDS.sleep(3);
}
static class Runner implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
DateFormat format = new SimpleDateFormat("HH:mm:ss");
while(true){
System.out.println(Thread.currentThread().getName()+" Run at"+format.format(new Date()));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
运行结果:
PrintThread Run at17:20:41
PrintThread Run at17:20:42
PrintThread Run at17:20:43
main suspend PrintThread at17:20:44
PrintThread Run at17:20:44
main resume PrintThread at17:20:47
PrintThread Run at17:20:48
PrintThread Run at17:20:49
main stop PrintThread at17:20:50
但是不建议使用 原因如下:
- 如在调用suspend()后线程不会释放已经占有的资源(比如锁),而是占有资源的情况下sleep,容易引发死锁。
- stop()方法终结线程时不会宝恒线程的资源正常释放,只是说有这个机会。
暂停恢复操作 可以由 等待/通知 机制来代替。
安全的终止线程
通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断的将线程停止,这样做会更优雅和安全。
线程间通信
线程开始执行便拥有了自己的栈空间。
volatile 和 synchronized关键字
java支持多个线程访问一个对象或者对象的成员变量 每个线程都拥有这些变量的拷贝,会导致一个线程看到的变量并不一定是最新的。
volatile可以用来修饰字段(成员变量) ,告知程序任何对该变量的访问都要从共享内存中获取,而且对他的改变必须同步刷新回共享内存,他能保证所有线程对该变量的可见性。一般涉及到多个线程要对一个变量访问就将其定为volatile。
synchronized可以修饰方法或以同步块的形式来进行使用,它确保多个线程在同一时刻,只能有一个线程处于方法或同步块中,保证了线程对变量访问的可见性和排他性。
加锁的本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
任何一个对象都拥有自己的监视器,当这个对象被同步块或同步方法调用时,执行方法的线程必须先获得该对象的监视器才能进入同步块或者同步方法。获取不到的线程将会被阻塞在同步快和同步方法的入口处(同步队列),进入BLOCKED状态。直到其他获得了锁的线程(前驱)释放了锁,这个释放操作会唤醒阻塞在同步队列中的线程,让其重新尝试获取锁。
等待/通知机制
java中典型的消息传递机制。是任何java对象所具备的。
一个线程A 调用了对象O的 wait()方法进入等待状态,而另一个线程B调用了对象O的 notify( )方法,线程A接到通知后从对象O的wait()方法返回,进而执行后续操作。两个线程通过对象O来完成交互,对象上的wait和notify的关系如同开关信号,用来完成等待方和通知方之间的交互工作。
等待/通知范式
等待方
- 获取对象锁
- 条件不满足就wait()
- 条件满足则执行对应逻辑
通知方
- 获得对象锁
- 改变条件
- 通知所有等待在对象上的线程
管道输入/输出流
区别于文件输入/输出流或者网络输入/输出流不同之处在于,主要用于线程之间的数据传输,传输媒介为内存。
其主要有四种实现 PipedOutputStream、PipedInputStream、PipedReader、PipedWriter,前两种面向字节后两种面向字符。
Therad.join()
线程A执行了thread.join()的含义是:当前线程A等待thread线程终止后才从thread.join()返回。这个方法也有超时方法的重载。
ThreadLocal
线程变量,是一个以ThreadLocal对象为键,任意对象为值得储存结构。一个线程可以根据ThreadLocal对象查询绑定在这个线程上的一个值。
线程应用实例
等待超时模式
在等待通知范式上 用 wait(long t)来进行超时等待。
数据库连接池
数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
线程池
线程池的本质是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,工作者县城不断地从工作队列上取工作来执行,当工作队列为空的时候,所有的工作者线程均等待在工作队列上。当有客户端提交了一个任务之后会通知任意一个工作者线程。
线程实现的三种方式
- 使用内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级进程混合实现
java线程的调度
java使用的线程调度方式是抢占式的调度,是系统自动完成的,但是我们还是可以建议系统给某些线程多分配或少分配时间---设置现成的优先级。但是线程优先级并不靠谱,因为java线程是通过映射到系统的原生线程上来实现的,所以线程调度的最终结果取决于操作系统,如上面线程优先级所提到的 ,线程优先级不能作为程序正确性的依赖,因为有些操作系统会完全不用理会优先级的定义。
线程安全
什么是线程安全?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和替换执行,也不需要进行额外的同步,或者在调用方法的时候进行任何其他协调操作,调用这个对象的行为都可以获得正确的结果,那么对象是线程安全的。
按照线程安全的安全程度可以把java中的对共享数据的操作分为以下五类
- 不可变
不可变的对象一定是线程安全的,比如final关键字在之前的总结中提到过只要一个不可变的独享被正确的构建出来(没有出现this逃逸的情况)其外部可见状态永远不可变。 - 绝对线程安全
实际上标明自己是线程安全的类,也不是绝对安全。比如Vector,因为他的add get size方法都被synchronized修饰的,但是即使所有方法都被修饰成同步的,也不意味着调用这些方法永远不需要同步手段了,因为多个线程调用这些方法如果不做同步手段。 - 相对线程安全
保证对一个对象单独地操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性,在java中大部分线程安全类都属于这种类型,如 hashtable vector等 - 线程兼容
指的是对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,如ArrayList、HashMap等 - 线程对立
指的是无论调用端是否采用同步措施,都无法在多线程环境中并发使用的代码。
线程安全的实现方法
同步指的是多个线程并发的访问共享数据时,保证共享数据在同一时刻只能被一个线程访问。
互斥是实现同步的一种手段。
互斥是因,同步是果,互斥是方法,同步是目的。
互斥同步
1. Synchronized的实现原理
Synchronized关键词在编译后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令,这两个字节码需要一个reference类型变量来指定需要锁定的对象,如果synchronized明确指明了锁定的对象,那这个reference就是这个对象的引用。如果没有明确指定,就看它修饰的是方法还是实例方法,去取对应的类方法或对象实例来作为锁定的对象。
执行monitorenter时,首先尝试获取对象锁,如果当前线程已经拥有了这个对象的锁,则将计数器+1,相应执行monitorexit的时候就将计数器-1。当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,知道对象锁被另一个线程释放为止。
前面也提到过因为java线程是通过映射到系统的原生线程上来实现的,,所以如果要阻塞或唤醒一个线程,都需要操作系统来帮忙,就需要从用户态转换到和心态中,转换状态会需要消耗很多处理器的时间。所以synchronized在java中是一个重量级的操作。不过也有相应的优化,就是在通知操作系统阻塞线程之前加入一段自选等待过程,避免频繁的切入和心态。
2.J.U.C中的ReentrantLock
重入锁,是一个表现在API层面的互斥锁,通过lock(),unlock()try/finally,来完成。而synchronized是表现在原生语法层面的互斥锁。reentrantLock中有一些更高级的功能如下:
- 等待可中断
就是线程等待持有锁的线程释放锁等了很久就可以选择放弃等待,去处理其他事情。 - 公平锁
多个线程等待同一个锁的时候,必须按照申请锁的时间顺序来一次获得锁,synchronized中的锁默认情况就是非公平的,有带布尔类型的构造函数可以使synchronized使用公平锁。 - 锁绑定多个条件
指的是一个ReentrentLock对象可以同时绑定多个Condition对象,而Synchronized中需要关联多个条件,则需要额外的添加锁,而ReentrantLock只需要多次调用newCondition()即可。
非阻塞同步
互斥同步的不足之处
互斥同步的主要原因就是进行线程的阻塞唤醒的消耗所带来的性能问题,所以这种同步也称为阻塞同步。互斥同步比较悲观,他认为只要不去进行正确的同步措施,就会出现问题,无论共享数据是否会出现竞争,都要进行加锁、用户态核心态转换、维护锁的计数器、检查被阻塞线程是否需要唤醒等操作。
基于冲突检测的乐观并发策略(非阻塞同步)
先进行操作,如果没有其他线程争用数据那么操作就成功;如果共享数据有其它线程争用,产生了冲突(冲突检测),那就再采取补偿措施(如不断重试,直到成功),这种乐观的并发策略并不需要把线程挂起,因此这种同步操作称为非阻塞同步。
以上说的操作 和 冲突检测这两个步骤具备原子性。而如果靠互斥同步来保证这个原子性就失去意义,所以需要靠硬件指令来完成这件事。如 测试并设置,获取并增加,交换,比较并交换(CAS)等。
CAS操作
其中CAS需要三个操作数,内存位置V、旧的预期值A、新值B。当且仅当V符合旧预期值A的时候处理器采用新值B去更新V上的值,否则不执行更新,但无论是否更新V的值都会返回旧值。
CAS操作由Unsafe类里面的compareAndSwapInt()、compareAndSwapLong()等几个方法包装提供。这个类不是提供给用户程序调用的类,只限制了启动类加载器加载的Class才能访问、因此如果不采用反射的手段就只能通过其他javaAPI来访问,如juc包里面的整数原子类,其中的CompareAndSet()和getAndIncreament()等方法。
以incrementAndGet()为例
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
其中包含一个无限循环,不断尝试将当前值加1操作。如果失败说明在执行compareAndSet()的时候这个值已经被修改就是V上的值与旧值不符合,就不进行替换操作,于是进入下一次循环重试。
CAS的不足之处
- ABA问题
它无法涵盖互斥同步的所有使用场景,它存在一个漏洞:如果V初次读取的时候是A值,被其他线程赋值后它仍然是A值,如它曾经被改成了B后来又被改成了A,CAS就会认为他没有变过。不过可以通过加入版本号来保证CAS的正确性、不过大多数时候ABA问题不会影响程序并发的正确性,并且如果有ABA问题,改用传统的互斥同步可能会比原子类更高效。 - 循环时间长导致系统开销大
- 只能保证一个共享变量的原子操作,多个共享变量的时候可以用锁,或者把多个共享变量合并成一个共享变量来操作 比如 i=2 j=a 合并之后 ij=2a然后进行CAS操作