Thread的三种实现方案
任何实现方法都必须使用start方法来启动线程
- 继承Thread类,实现run方法
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run() {
for (int i=0;i<5;i++){
System.out.println(this.name+"hahahha");
}
}
}
public static void main(String[] args) {
new MyThread("AAA").start();
new MyThread("BBB").start();
new MyThread("CCC").start();
}
- 实现Runable接口,因为Thread类实现了Runable方法,可以接收的参数为Runable
class MyThread implements Runnable{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run() {
for (int i=0;i<5;i++){
System.out.println(this.name+"hahahha");
}
}
}
public static void main(String[] args) {
new Thread(new MyThread("AAA")).start();
new Thread(new MyThread("BBB")).start();
new Thread(new MyThread("CCC")).start();
}
- 实现Callable接口,因为futureTask实现了RunnableFuture接口,而该接口又继承了Runable接口和future接口。Runable接口负责多线程,future借口负责返回值,因此将callable传入futureTask,然后再将futureTask传入Thread即可实现多线程。
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
for (int i=0;i<5;i++){
System.out.println("hahahha");
}
return "线程执行完毕";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task=new FutureTask<String>(new MyThread());
new Thread(task).start();
System.out.println(task.get());
}
线程的方法
线程命名:new Thread(Runable,线程名字)
,如果不设置名字则会自动生成一个不重复的名字
获取线程名字:Thread.currentThread().getName()
线程的休眠:sleep(毫秒[,纳秒])
,但是有可能在休眠时候产生中断异常,可以捕获的异常
判断是否中断:isInterupted()
,即判断打断标记是否为true,正常线程被打断为true
判断是否中断:Thread.interupted()
,即判断打断标记是否为true,正常线程被打断为true,该方法会将打断标记设置为false
线程中断:interupt()
,可以正常运行的线程或者打断处于阻塞状态的线程,比如wait、sleep、join。打断正常运行的线程打断标记设置为true,但是程序不会停止。打断阻塞状态的线程打断标记重新设置为false,程序会停止
线程强制执行:join()
,在其他的线程中加入某个需要执行的线程
线程礼让:yield()
,但每次判断只会礼让一次
设置守护线程:setDaemon(true)
设置守护线程,当整体程序执行完毕后,守护线程无论执行完与否都会停止,Java的GC就是最大的守护线程。
如何优雅的停止线程
循环判断当前线程是否被打断,如果被打断的话则执行后续的操作;如果未打断的话则让其睡眠几秒,如果在睡眠期间被打断会抛出异常,此时打断标记会变为false,则设置打断标记为true,并且下一次判断时打断标记就是true,可以执行后续操作。如果睡眠之后被打断,那么下一次进入循环则直接判断。
class Monitor{
private Thread monitor;
public void start(){
monitor=new Thread(()->{
while (true){
Thread current=Thread.currentThread();
if (current.isInterrupted()){
System.out.println("结束");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
current.interrupt();
e.printStackTrace();
}
}
});
monitor.start();
}
public void end(){
monitor.interrupt();
}
}
wait-notify原理
这两种状态下的线程都不会占用CPU
- Waiting:调用了wait方法会进入该状态
- Blocked:获取不到锁的线程的状态
调用了owner中的线程发现条件不满足,调用wait方法进入WaitSet。当被唤醒的时候,会进入到EntryList中重新竞争锁。
调用wait和notify、notifyall方法之前必须先获得对象的锁。
线程优先级
优先级越高的线程越有可能先执行,但不是必定先执行
优先等级:
- 最高:MAX_PRIORITY,值为10
- 中等:NORM_PRIORITY,值为5(线程默认优先级)
- 最低:MIN_PRIORITY,值为1
设置优先级:setPriority(int x)
获取优先级:getPriority()
线程的6大状态及转换
- NEW:表示线程刚刚创建,还没有start
- RUNNABLE:表示已经start,包含了操作系统层面的运行状态、就绪状态、阻塞状态
- BLOCKED:线程竞争锁失败时进入到该状态
- WAITING:表示该线程进入了monitor对象的WaitSet,再等待状态
- 当调用对象的wait方法时从runnable进入waiting状态,当被notify、notifyAll或interrupt时,如果获取到了锁则进入runnable状态,竞争不到锁时会进入blocked状态
- 当前线程调用了某个线程的join方法之后,当前线程从runnable进入waiting,当对应线程执行完毕或者调用了当前线程的interrupt方法后会让目标线程进入runnale
- 当前线程调用LockSupport.park方法会让当前线程进入waiting状态,调用LockSupport.unpark(目标线程)方法再回到runnable状态
- TIME_WAITING:超时等待
- 调用wait(时间)时,线程状态变为TIME_WAITING,当到了时间或者调用了notify、notifyAll或interrupt时会竞争锁,如果获取到了锁则进入runnable状态,竞争不到锁时会进入blocked状态
- 调用join(time)会让该线程进入TIME_WAITING,时间到了或者掉用了interrupt时会变为runnable
- 调用了sleep(time)时进入该状态,睡眠过后变为runnable
- 调用LockSupport.parkNanos(nanos)或LockSupport.parkUtil(time)时进入该状态,调用unpark或者interrupt时进入runnable状态
- TERMINATED:线程代码执行完毕后的状态
synchronized优化
在java HotSpot虚拟机里,每个对象都有对象头(包括class指针和MarkWord)。MarkWord平时存储这个对象的哈希值、分代年龄,当加锁时,这些信息就根据情况被替换为标记位(即锁的类型)、锁记录指针、重量级锁指针、线程ID等内容。每个线程的栈帧都包含一个锁记录的结构,内部可以存储锁定对象的Markword。
-
轻量级锁:如果一个对象有多个对象访问,但是多个线程的访问时间是错开的,即没有锁的竞争,那么线程对该对象上锁时加轻量级锁可以优化(当关闭了偏向锁时,当在一个线程内锁同一对象即发生了锁重入时也仍然是轻量级锁)。给对象加锁时,首先检查对象加锁状态,若为01,表示无锁,那么线程会首先将对象的markword复制到锁记录中,然后用CAS将对象头的Mark替换为线程的锁记录地址。如果成功的替换,那么上锁成功。对象状态变为00(轻量级锁),当同步代码块执行完毕后,解锁时再将对象的状态改为01,
-
重量级锁:如果一个对象有多个对象访问,但是多个线程的访问时间有重叠,有锁的竞争,那么线程对该对象上锁时从轻量级锁变为重量级锁。当线程在用CAS修改对象的Markword时,如果失败的话那么会发生锁膨胀,将轻量级锁升级为重量级锁。将对象的状态改为10(线程阻塞中),并且在对象头里加入重量级锁的指针(该指针为了找到要唤醒的线程)。当上一个线程执行完毕后,会尝试解锁,但是因为锁已经升级为重量级锁,因此会解锁失败,接着会释放重量级锁,唤起阻塞的线程竞争。
-
自旋锁:自旋锁可以继续优化重量级锁, 当众多的线程竞争重量级锁时,其中的一个获取了锁,那么其他的线程不会立即阻塞(因为阻塞需要保持当前状态,耗时),而是会自旋重试获取重量级锁。当重试到了一定的次数后才会阻塞。java6之后的自旋次数是自适应的,根据实际情况会调整。自旋会占用CPU时间,因此只适用于多核CPU
-
偏向锁: 轻量级锁在有锁重入的情况时,每次重入仍然要耗时执行CAS操作,java6引入了偏向锁来做进一步的优化,只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的,就表示没有竞争,不用重新CAS。当有其他线程要对这个对象加锁时,会撤销偏向锁,变为轻量级锁。当撤销次数超过20次时,会将该对象继续转变为偏向锁。当偏向次数超过40次时,JVM会认为偏向是错误的,因此会撤销该类的所有对象的偏向锁。
缺点:撤销偏向锁要将锁升级为轻量级锁,这个过程需要STW;其次访问对象的hashcode也会撤销偏向锁,调用对象的wait方法也会撤销偏向锁等
其他优化:
- 减少上锁时间,即被锁的代码越少越好
- 减小锁的粒度,比如ConcurrentHashMap
- 锁粗化,详见StringBuffer
- 锁清除:比如某个加锁对象是方法内部的局部变量,那么它不会被其他线程访问到,因此这时即时编译器就会忽略掉所有的同步操作。
- 读写分离
生产者消费者模型
class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource){
this.resource=resource;
}
@Override
public void run() {
for (int i=0;i<100;i++){
try {
resource.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource){
this.resource=resource;
}
@Override
public void run() {
for (int i=0;i<100;i++){
try {
resource.rem();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Resource{
private int num=0;
public synchronized void add() throws InterruptedException {
while (this.num>0){
super.wait();
}
Thread.sleep(500);
this.num++;
System.out.println("num="+this.num);
super.notifyAll();
}
public synchronized void rem() throws InterruptedException {
while (this.num<1){
super.wait();
}
this.num--;
System.out.println("num="+this.num);
super.notifyAll();
}
}