侧边栏壁纸
  • 累计撰写 269 篇文章
  • 累计创建 141 个标签
  • 累计收到 16 条评论

目 录CONTENT

文章目录

[笔记]Java 多线程编程核心技术

Sherlock
2018-05-17 / 0 评论 / 0 点赞 / 1095 阅读 / 0 字
温馨提示:
本文最后更新于2023-10-09,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1.Java 多线程技能

1.使用 interrupt 方法中断线程,不要使用 stop、suspend等方法(不安全、且已被废弃)

当对一个线程调用了 interrupt()之后,如果该线程处于被阻塞状态(比如执行了wait、sleep或join等方法),那么会立即退出阻塞状态,并抛出一个 InterruptedException异常,在代码中catch这个异常进行后续处理。如果线程一直处于运行状态,那么只会把该线程的中断标志设置为 true,仅此而已,==所以 interrupt()并不能真正的中断线程==

2.如果使用 stop 强制让线程停止,则有可能使一些清理性的工作得不到完成,另外一种情况就是对锁定对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的情况问题 3.this.interrupted(): 测试当前线程(运行该方法的线程)是否已经是中断状态,执行后具有将状态标志置清除为 false 的功能 4.this.isInterrupted(): 测试线程 Thread 对象是否已经是中断状态,但不清除状态标志 5.如果在线程 sleep 状态下停止某一线程,会触发 InterruptedException ,并且清除停止状态值,使之变为 false 6.建议使用抛异常(InterruptedException)法来实现线程的停止 7.suspendresume (暂停/恢复)方法已经被废弃,缺点——独占、不同步 8.yield() 方法的最用是放弃当前的 CPU 资源,将他让给其他的任务去占用 CPU 执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。 9.在操作系统中,线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就是 CPU 优先执行优先级较高的线程对象中的对象 10.设置线程的优先级使用 setPriority(),java 中分为1~10这10个等级,不在范围内会抛出 IllegalArgumentException 异常,最小是 1,最大是 10,默认是 ==5== 12.线程优先级具有==继承性==,比如 A 线程启动 B 线程,则 B 线程的优先级与 A 是一样的。 13.线程的优先级还具有 “随机性” ,也就是优先级较高的线程不一定每次都先执行完 14.守护线程的作用是为其他线程的运行提供便利服务,最典型的应用就是 GC (垃圾回收器),他就是一个很称职的守护者。thread.setDaemon(true)

2.对象及变量的并发访问

1.方法中的变量不存在非线程安全问题,永远都是线程安全的(方法内部的变量是私有的特征造成的)。 2.对象的实例变量非线程安全,在多个线程访问同一个对象中的==同步方法==时一定是线程安全的。调用关键字synchronized声明的方法一定是排队运行的(必须是同一个对象,对个对象会有多把锁)。 3.只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有就同步的必要。 4.关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当做锁。 5.==脏读==一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果 6.关键字synchronized拥有==锁重入==的功能,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的——在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。 7.==可重入锁==的概念是:自己可以再次获取自己的内部锁。可重入锁也支持在==父子类继承==的环境中。 8.当一个线程执行的代码出现==异常==时,其所持有的==锁会自动释放== 9.synchronized同步==不==具有==继承性== 10.synchronized==方法==是对==当前对象==进行加锁,synchronized==代码块==是对==某一个对象==进行加锁 11.和synchronized方法一样,synchronized (this)代码块也是锁定当前对象的 12.synchronized (非 this 对象 X)格式的写法是将 X 对象本身作为“对象监视器”

  • 当多个线程同时执行synchronized (X) {} 同步代码块时呈同步效果
  • 当其他线程执行 X 对象中synchronized 同步方法时呈同步效果
  • 当其他线程执行 X 对象方法里面的synchronized (this) {} 代码块时也呈同步效果

但需要注意:如果其他线程调用不加synchronized关键字的方法时,还是异步调用
13.synchronized关键字用在静态方法上是给 Class 类上锁,而synchronized关键字用在非静态方法上是给对象上锁,而 Class 锁可以对类的所有对象实例起作用。

synchronized (class) {}同步代码块和synchronized static () {}方法的作用一样

14.在将任何数据类型作为同步锁时,如果有多个线程同时持有相同的锁对象,则这些线程之间就是同步的(只要锁对象不变,即使对象的属性被改变,还是同步的);如果分别获得锁对象,这些线程之间就是异步的。 15.volatile关键字的主要作用是使变量在多个线程间可见,致命缺点==不支持原子性==。

  • 1.volatile关键字是线程同步的轻量级实现,性能比synchronized好,并且只能修饰于变量
  • 2.多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
  • 3.volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据同步。
  • 4.volatile解决的是变量在多个线程之间的可见性;而synchronized解决的是多个线程之间访问资源的同步性。
  • 5.线程安全包括原子性和可见性两个方面。

16.synchronized可以保证互斥性和可见性,同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

3.线程间通信

3.1 等待/通知机制

1.wait() 方法的作用是使当前执行代码的线程进行等待,是 Object 类的方法,用来将当前线程植入“预执行队列”中,并且在wait()所在的代码处停止执行,知道接到通知或被中断为止。 2.在调用 wait() 方法之前,线程必须先获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait() 方法。在执行 wait() 方法后,当前线程释放锁。在从wait()返回前,线程和其他线程竞争重新获取锁。如果调用 wait() 时没有持有适当的锁,则抛出IllegalMonitorStateException异常(继承了RuntimeException)。 3.wait() 方法可以使调用该方法的线程==释放==共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。 4.notify() 方法可以随机唤醒等待队列中等待==同一==共享资源的 “一个” 线程,并使该线程退出等待队列,进入可运行状态,也就是 notify() 方法仅通知 “一个” 线程。 5.notifyAll() 方法可以使所有正在等待队列中等待==同一==共享资源的 “全部” 线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于 JVM 虚拟机的实现。 6.每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列储存了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待 CPU 的调度;反之,一个线程被 wait 后,就会进入阻塞队列,等待下一次被唤醒。 7.当方法 wait() 被执行后,锁被自动释放,但执行完 notify() 方法,锁却不自动释放,必须执行完 notify() 方法所在的同步 synchronized 代码(块)后才锁。 8.当线程呈 wait() 状态时,调用线程对象的 interrupt() 方法会出现 InterruptedException 异常。 9.执行完同步代码块就会释放对象的锁,在执行完同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。 10.为了唤醒全部线程,可以使用 notifyAll() 方法。 11.带一个参数的 ==wait(long)== 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。 12.使用 notify() 方法要防止通知过早,通知过早会打断程序正常的运行逻辑。 13.生产者和消费者模型中:注意用 while 代替 if(使用 if 会出现条件发生改变时没有得到及时响应,造成多个呈 wait 状态的线程被唤醒);注意使用 notifyAll() 代替 notify(),解决“假死”(假死主要原因是有可能连续唤醒同类了) 14.可以使用管道流进行线程间通信:字节流(PipedOutputStream/PipedInputStream)、字符流(PipedWriter/PipedReader)—— PipedOutputStream.connect(PipedInputStream)

3.2 方法 join 的使用

1.方法 join 的作用是等待线程对象销毁,使所属的线程对象 X 正常执行 run() 方法中的任务,而使当前线程 Z 进行无限期的阻塞,等待线程 X 销毁后再继续执行线程 Z 后面的代码。 2.方法 join 具有使线程排队运行的作用,有些类似同步的运行效果。join 与 synchronized 的区别是:join 在内部使用 wait() 方法进行等待,而 synchronized 关键字使用的是“对象监视器”原理作为同步。 3.在 join 过程中,如果当前线程 Z 对象被中断,则当前线程出现异常,而线程 X 会继续运行。 4.方法 join(long) 中的参数是设定等待的时间,内部是使用 wait(long) 方法来实现的,所以 join(long) 方法具有释放锁的特点,而 Thread.sleep(long) 方法却不释放锁。

3.3 类 ThreadLocal 的使用

http://gblog.sherlocky.com/threadlocal/
1.自定义类继承 ThreadLocal 类,覆盖 initialValue() 方法可以使第一次调用 get() 结果不再为null

3.4 类 InheritableThreadLocal 的使用

1.使用类 InheritableThreadLocal 可以让子线程从父(主)线程中继承值。 2.同样的,覆盖 InheritableThreadLocal.initialValue() 方法也可以使第一次调用 get() 结果不再为null 3.通过覆盖InheritableThreadLocal.childValue()方法还可以让子线程修改值。 4.如果子线程在取得值的同时,父(主)线程将 InheritableThreadLocal 中的值进行更改,那么子线程取到的值还是旧值。

4.Lock 的使用

4.1 使用 ReentrantLock 类

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyService {
	private Lock lock = new ReentrantLock();
	public void testMethod() {
		lock.lock();
		for (int i = 0; i < 5; i++) {
			System.out.println("ThreadName=" + Thread.currentThread().getName()
					+ (" " + (i + 1)));
		}
		lock.unlock();
	}
}

1.JDK 1.5中新增加了 ReentrantLock 类也可以达到 synchronized 关键字的效果,并且在扩展功能上也更加强大——嗅探锁定、多路分支通知... 2.调用 lock.lock() 代码的线程就持有了“对象监视器”,其他线程只有等待锁释放时再次争抢。效果和使用 synchronized 关键字一样,线程之间执行的顺序是随机的。 3.借助 Condition 对象, ReentrantLock 也可以实现等待/通知功能。 Condition(JDK5+) 可以实现多路通知功能 4.在一个 Lock 对象里面可以创建多个 Condition(即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class MyService {
	private ReentrantLock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();
	public void waitMethod() {
		try {
			lock.lock();
			System.out.println("A");
			// 调用 condition.await 之前,需要先调用 lock.lock() 获得同步监视器,否则会报 IllegalMonitorStateException 
			condition.await();
			System.out.println("B");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
			System.out.println("锁释放了!");
		}
	}
}

5.Object 类中的 wait() 方法相当于 Condition 类中的 await() 方法 Object 类中的 wait(long timeout) 方法相当于 Condition 类中的 await(long time, TimeUnit unit) 方法 Object 类中的 notify() 方法相当于 Condition 类中的 signal() 方法 Object 类中的 notifyAll() 方法相当于 Condition 类中的 signalAll() 方法 6.其实 Condition 对象可以创建多个


public class MyService {
	private Lock lock = new ReentrantLock();
	public Condition conditionA = lock.newCondition();
	public Condition conditionB = lock.newCondition();

	public void awaitA() {
		try {
			lock.lock();
			System.out.println("begin awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.await();
			System.out.println("  end awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void awaitB() {
		try {
			lock.lock();
			System.out.println("begin awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.await();
			System.out.println("  end awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_A() {
		try {
			lock.lock();
			System.out.println("  signalAll_A时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.signalAll();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_B() {
		try {
			lock.lock();
			System.out.println("  signalAll_B时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.signalAll();
		} finally {
			lock.unlock();
		}
	}
}

// 测试类代码可以这么写
	public static void main(String[] args) throws InterruptedException {
		MyService service = new MyService();
		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();
		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();
		Thread.sleep(3000);
		// 只唤醒了 A 线程
		service.signalAll_A();
	}

7.公平锁与非公平锁:锁 Lock 分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的 FIFO 先进先出顺序。而非公平锁(ReentrantLock默认)就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。 8.一些常用方法

  • int lock.getHoldCount() : 查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数
  • int lock.getQueueLength() : 返回正在等待获取此锁定的线程估计数
  • int lock.getWaitQueueLength(Condition con) : 返回等待与此锁定相关的给定条件 Condition 的线程估计数

  • boolean lock.hasQueuedThread(Thread t) : 查询指定的线程是否正在等待获取此锁定
  • boolean lock.hasQueuedThreads() : 查询是否有线程正在等待获取此锁定
  • boolean lock.hasWaiters(Condition con) : 查询是否有线程只在等待与此锁定相关的 condition 条件

  • boolean lock.isFair : 判断是不是公平锁
  • boolean lock.isHeldByCurrentThread() : 查询当前线程是否保持此锁定(当前线程是否获取此锁)
  • boolean lock.isLocked : 查询此锁定是否由任意线程保持(是否有线程持有该锁)

  • void lock.lockInterruptibly() : 如果当前线程未被中断,则获取锁定;如果已经被中断则出现异常
  • boolean lock.tryLock() : 仅在调用时锁定未被一个线程保持的情况下,才获取该锁定(否则直接返回 false)
  • boolean lock.tryLock(longt timeout, TimeUnit unit) : 如果锁定在给定等待时间内没有被另外一个线程保持,且当前线程没有被中断,则获取该锁定

  • condition.awaitUninterruptibly() : 使用该方法 await 时,线程如果被中断,不出现异常
  • condition.awaitUntil(Date deadline) : 线程最多只 await 到 deadline时刻(线程在等待时间到达前,也可以被其他线程提前唤醒)

9.可以使用 Condition 对象可以对线程执行的业务进行排序规划(使用多个 Condition)

synchronized有个锁升级机制(具体可参考第8章),是 JVM 层面的,而且在代码执行出现异常时,JVM 会自动释放锁定; ReentantLock是 JDK 层面的,不会自动释放锁,必须unLock

4.2 使用 ReentrantReadWriteLock 类

1.类 ReentrantLock 具有完全互斥排他的效果,即同一时间只有一个线程在执行 ReentrantLock.lock() 方法后面的任务(保证了实例变量的线程安全性,但效率低下), JDK 中提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率。在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReentrantReadWriteLock 来提升该方法的运行效率。

2.常用场景

  • 读读共享 lock.readLock().lock() : 允许多个线程同时执行 lock() 方法后面的代码
  • 写写互斥 lock.writeLock().lock() : 同一时间只允许一个线程执行 lock() 方法后面的代码
  • 读写互斥
  • 写读互斥

==只要出现写就是互斥的==

Lock 是 synchronized 关键字的进阶(完全可以替换),在并发包中大量的类使用了 Lock 接口作为同步的处理方式。

4.3 Java 开发手册(阿里巴巴)对 Lock 使用的要求:

【强制】 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代 码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。

  • 说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功 获取锁。
  • 说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中, unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。
  • 说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。

正例:

Lock lock = new XxxLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}

反例:

Lock lock = new XxxLock();
// ...
try {
    // 如果此处抛出异常,则直接执行 finally 代码块
    doSomething();
    // 无论加锁是否成功,finally 代码块都会执行
    lock.lock();
    doOthers();
} finally {
    lock.unlock();
}

【强制】 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是 否持有锁。锁的释放规则与锁的阻塞等待方式相同。

说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。

正例:

Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
        doSomething();
        doOthers();
    } finally {
        lock.unlock();
    }
}

5.定时器 Timer

5.1 定时器 Timer 的使用

1.Timer 类的主要作用就是设置计划任务,但封装任务的类却是 TimerTask 类(抽象类),执行计划任务的代码要放入 TimerTask 的子类中。 2.timer.schedule(TimerTask task, Date time) : 在指定的日期执行一次某一任务。允许有多个 TimerTask 任务及延时。 3.TimerTask 是以队列的方式一个一个被顺序性地执行的(多个 task 时),所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行的时间也被延后。 4.timer.schedule(TimerTask task, Date firstTime, long period) : 在指定的日期之后按指定的间隔周期,无限循环地执行某一任务。(如果间隔时长小于每次执行所需时长,则执行完一次任务后会立即开始下一次任务) 5.TimerTask 类的 cancel() 方法是将自身从任务队列中被移除。 6.Timer 类的 cancel() 方法作用是将任务队列中全部的任务进行清空。

Timer 类中的 cancel() 方法有时并不一定会停止计划任务,而是正常执行——有时候 cancel() 方法并没有争抢到 queue 锁,则让 TimerTask 类中的任务正常执行了。

7.timer.schedule(TimerTask task, long delay) : 以执行该方法当前的时间为参考时间,在此时间基础上延迟执行的毫秒数后执行一次 TimerTask 任务 8.timer.schedule(TimerTask task, long delay, long period) : 以执行该方法当前的时间为参考时间,在此时间基础上延迟执行的毫秒数,再以某一时间间隔无限次数地执行 TimerTask 任务

凡是使用方法中带有 ==period== 参数的,都是无限循环执行 TimerTask 中的任务。

9.timer.scheduleAtFixedRate(TimerTask task, Date firstTime, long period) : 方法 schedule 和 scheduleAtFixedRate 都会按顺序执行,所以不要考虑非线程安全的情况。两者主要的区别只在与有没有追赶特性。 10.追赶性主要体现在:如果任务计划运行时间早于当前时间,则将两个时间段内的时间多对应的 Task 任务被==“补充性”==地执行,这就是 Task 任务追赶特性。

5.2 Java开发手册(阿里巴巴)对 Timer 的要求

【强制】 多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,如果在处理定时任务时使用ScheduledExecutorService 则没有这个问题

6.单例模式与多线程

6.1 立即加载/“恶汉模式”

1.立即加载:就是使用类的时候已经将对象创建完毕,常见的实现方法就是直接 new 实例化。在调用方法前,实例已经被创建了。

public class HungerSingleton {
    // 立即加载方式==饿汉模式
    private static HungerSingleton instance = new HungerSingleton();

    // 私有构造方法
    private HungerSingleton() {}

    public static HungerSingleton getInstance() {
        // 此代码版本为立即加载
        // 此版本代码的缺点是不能有其它实例变量
        // 因为getInstance()方法没有同步
        // 所以有可能出现非线程安全问题
        return instance;
    }
}

6.2 延迟加载/“懒汉模式”

1.延迟加载:就是使用在调用 XX 方法时实例才被创建,常见的实现方法就是在 XX 方法中进行 new 实例化。

懒汉模式实现单例模式,推荐使用 DCL(Double-Check Locking) 双检查锁 机制来解决多线程环境中的非线程安全问题。即保证了不需要同步的代码的异步执行性(效率),又保证了单例的效果。

public class LazySingleton {
    // volatile 关键字 防止重排序
    private static volatile LazySingleton instance;
    // 对于 ``long`` 和 ``double`` 的基本类型,双重检查模式仍然是适用的
    // private static long instance;
    
    // 私有构造方法
    private LazySingleton() {}
   
    // 双重校验锁
    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

需要注意的是,引用类型使用双重检查锁有可能会失效,是因为指令重排造成的。直接原因也就是初始化一个对象并使一个引用指向他这个过程不是原子的。导致了可能会出现引用指向了对象并未初始化好的那块堆内存,此时,使用==volatile==修饰对象引用,防止重排序即可解决。对于 longdouble 的基本类型,双重检查模式仍然是适用的。可参考【双重检查锁定与延迟初始化】一文。

6.3 使用静态内置类实现单例模式

public class InnerClassSingleton {

    private static class InnerClassSingletonHandler {
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    
    // 私有构造方法
    private InnerClassSingleton() {}
    
    public static InnerClassSingleton getInstance() {
        return InnerClassSingletonHandler.instance;
    }
}

静态内置类可以解决线程安全问题,但如果遇到序列化对象事,使用默认的方式运行得到的结果还是多例的。
解决办法就是在反序列化中使用 readResolve() 方法

6.4 序列化与反序列化的单例模式实现

public class InnerClassSingleton implements Serializable {
    private static final long serialVersionUID = 2049102434160727122L;

    // 内部类方式
    private static class InnerClassSingletonHandler {
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }
    
    // 私有构造方法
    private InnerClassSingleton() {}
    
    public static InnerClassSingleton getInstance() {
        return InnerClassSingletonHandler.instance;
    }
    
    // 反序列化时使用 readResolve 方法可以解决反序列化后多例问题
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return InnerClassSingletonHandler.instance;
    }
}

6.5 使用 static 代码块实现单例模式

静态代码快中的代码在使用类的时候就已经执行了,所以可以应用静态代码开的这个特点来实现单例设计模式。

public class StaticCodeBlockSingleton {
    private static StaticCodeBlockSingleton instance = null;
    
    // 私有构造方法
    private StaticCodeBlockSingleton() {}
    
    static {
        instance = new StaticCodeBlockSingleton();
    }
    
    public static StaticCodeBlockSingleton getInstance() {
        return instance;
    }
}

6.6 使用 enum 枚举数据类型实现单例模式

枚举 enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例单例模式。

public class EnumSingletonClazz {

    public enum EnumSingleton {
        instanceFactory;
        
        // 此处使用 String 简化代码
        private String instance;
        
        private EnumSingleton() {
            System.out.println("创建 EnumSingletonClazz 对象");
            instance = new String("EnumSingletonClazz");
        }
        
        public String getInstance() {
            return instance;
        }
    }
    
    public static String getInstance() {
        return EnumSingleton.instanceFactory.getInstance();
    }
}

7.拾遗增补

7.1 线程的状态

1.线程对象在不同的运行时期有不同的状态,状态信息就存在于 java.lang.Thread.State 枚举类中 线程有关的方法与线程状态关系示意图 在调用与线程有关的方法后,线程会进入不同的线程状态,这些状态之间某些是可双向切换的,比如 WAITING 和 RUNNING 状态之间可以循环地进行切换;而有些是单向切换的,比如线程销毁后并不能自动进入 RUNNING 状态。

2.java中线程的 6 大状态

  • 1.NEW(初始) :新创建了一个线程对象(实例化后),但还没有调用start()方法时的状态
  • 2.RUNNABLE(运行) :Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)

  • 3.BLOCKED(阻塞) :线程在等待锁的状态(阻塞于锁)
  • 4.WAITING :执行了 Object.wait() 方法后所处的状态

进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

  • 5.TIMED_WAITING(超时等待) :等待状态 (执行了 Thread.sleep() 方法,不同于 WAITING,可以在指定的时间内自行返回)
  • 6.TERMINATED(终止) :线程已经执行完毕被销毁时的状态

3.线程的状态图

7.2 线程组

1.可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以由线程组,组中还可以有线程。有些类似于树的形式。 2.线程组的作用是:可以批量的管理线程或线程组对象,有效地对线程或线程组对象进行组织。 3.线程对象关联线程组;支持1级关联也支持多级关联(不利于管理,但 JDK 提供了支持)。 4.线程组具有自动归属特性。 5.JVM 的根线程组就是 system,再取其父线程组则出现NPE。 6.线程组内的线程可以批量停止:通过将线程归属到线程组中,当调用线程组 ThreadGroup 的 interrupt() 方法时,可以将该组中的所有正在运行的线程批量停止。

7.3 使线程具有有序性

正常的情况下,线程在运行时多个线程之间执行任务的时机是无序的。可以通过改造代码的方式使他们运行具有有序性。

7.4 SimpleDateFormat 非线程安全

该类主要负责日期的转换与格式化,但在多线程的环境中,使用此类极易造成数据转换及处理的不准确,因为它非线程安全。

创建多个 SimpleDateFormat 类的实例即可解决该问题。

也可以使用 ThreadLocal 解决该问题。

7.5 线程中出现异常的处理

1.可以对多线程中的异常进行“捕获”,使用 UncaughtExceptionHandler 类,可以对发生的异常进行有效的处理, thread.setUncaughtExceptionHandler() 是给指定线程对象设置的异常处理器。

{
	MyThread t1 = new MyThread();
	t1.setName("线程t1");
	t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			System.out.println("线程:" + t.getName() + " 出现了异常:");
			e.printStackTrace();
		}
	});
	t1.start();
	MyThread t2 = new MyThread();
	t2.setName("线程t2");
	t2.start();
	System.out.println("后续代码。。。");
}

2.XXXThread.setDefaultUncaughtExceptionHandler() 方法是对所有线程对象设置默认的异常处理器。

{
	MyThread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
				@Override
				public void uncaughtException(Thread t, Throwable e) {
					System.out.println("线程:" + t.getName() + " 出现了异常:");
					e.printStackTrace();
				}
			});
	MyThread t1 = new MyThread();
	t1.setName("线程t1");
	t1.start();
	MyThread t2 = new MyThread();
	t2.setName("线程t2");
	t2.start();
	System.out.println("后续代码。。。");
}

8.synchronized 锁的升级

以前我们都知道synchronized性能好,但是 Jdk 1.8 升级之后反而多了很多synchronized的使用,这是因为:使用了锁升级。

synchronized之前一直都是重量级的锁,但是后来 java 官方对他进行了升级,升级后采用的是锁升级的方式。 就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。 最后如果以上都失败就升级为重量级锁。

所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

8.1 锁的升级

锁的 4 种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)。

8.1.1 偏向锁:

  • 为什么要引入偏向锁?

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

  • 偏向锁的升级 当线程 1 访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID。由于偏向锁不会主动释放锁,因此以后线程 1 再次获取锁的时候,需要比较当前线程threadID和 Java 对象头中的threadID是否一致

如果一致(还是线程 1 获取锁对象),则无需使用CAS来加锁、解锁; 如果不一致(其他线程,如线程 2 要竞争锁对象,而偏向锁不会主动释放,因此还是存储的线程 1 的threadID),那么需要查看Java对象头中记录的线程 1 是否存活

如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁; 如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程 1,撤销偏向锁,升级为轻量级锁,如果线程 1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

  • 偏向锁的取消: 偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0

如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking=false来设置取消偏向锁。

8.1.2 轻量级锁

  • 为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。

因为阻塞线程需要 CPU 从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

  • 轻量级锁什么时候升级为重量级锁?

线程 1 获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程 1 的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程 1 存储的锁记录(DisplacedMarkWord)的地址;

如果在线程 1 复制对象头的同时(在线程1 CAS之前),线程 2 也准备获取锁,复制了对象头到线程 2 的锁记录空间中,但是在线程 2 CAS的时候,发现线程 1 已经把对象头换了,线程 2 的CAS失败,那么线程 2 就尝试使用自旋锁来等待线程 1 释放锁。

但是,如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,比如 10 次或者 100 次。

如果自旋次数到了线程 1 还没有释放锁,或者线程 1 还在执行,线程 2 还在自旋等待,这时又有一个线程 3 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁

重量级锁会把除了拥有锁的线程都阻塞,防止 CPU 空转。

==注意==:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。 一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

8.1.3 这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)

几种锁的优缺点.jpg

8.2 锁粗化

加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。 

锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

8.3 锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区