Java 多线程编程核心技术

Java 多线程编程核心技术

文章目录

一、Java 多线程技能

1. 进程和线程的定义及多线程的优点

进程: 进程是操作系统的基础,是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

线程: 可以理解为在一个进程中独立运行的子任务。

进程与线程的总结:

  • 进程虽然是互相独立的,但它们可以互相通信的,较为通用的方式是使用 Socket 或 HTTP 协议。
  • 进程拥有共享的系统资源,比如内存,网络端口,供其内部线程使用。
  • 进程较重,因为创建进程需要操作系统分配资源,会占用内存。
  • 线程存在于进程中,是进程的一个子集,现有进程,后有线程。
  • 虽然线程较轻,但是线程的上下文切换的成本非常高。

2. 多线程的创建

实现多线程的方式主要有俩种:

  1. 继承 Thread 类。
  2. 实现 Runnable 接口。

1. 继承 Thread 类

Thread 类的部分源码如下:

public class Thread implements Runnable {

	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

	@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

通过继承 Thread 实现多线程:

public class MyThread extends Thread {
	
	@Override
	public void run(){
	
		System.out.println("MyThread");
	}
}

public class Run(){
	public static void main(String[] args){
		
		MyThread myThread = new MyThread();
		myThread.start();
		System.out.println("运行结束!");
	}
}

运行结果:

运行结束!
MyThread

使用继承 Thread 类创建线程的缺点:

  • 不支持多继承,因为 Java 语言只支持单继承,所以为了支持继承多继承,可以实现 Runnable 接口的方式。

上述代码案例显示出 start() 方法比较耗时,增加了先输出 “运行结束” 的概率。方法 start() 更耗时的原因是内部执行了好几个步骤:

  1. 通过 JVM 告诉操作系统创建 Thread。
  2. 操作系统开辟内存并使用 Windows SDK 中的 createThead() 函数创建 Thread 线程对象。
  3. 操作系统对 Thread 线程对象进行调度,以确定执行时机。
  4. Thread 在操作系统中被成功执行。

Thread 的 start() 方法通知"线程规划期" —此线程已经准备就绪,准备调用线程的 run() 方法。这个过程其实就是让操作系统安排一个时间来调用 Thread 中的 run() 方法执行具体的任务,具有异步随机顺序执行效果。如果调用的是 thread.run() 而不是 thread.start() 方法,那就是同步执行了。

2. 实现 Runnable 接口

Runnable 接口源码如下:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

通过实现 Runnable 接口实现多线程:

public class MyRunnable implements Runnable{
	
	@Override
	public void run(){
		System.out.println("运行中");
	}
}

public class run{
	
	public static void main(String[] args){
		Runnable runnable = new MyRunnable();
		Thread thread = new Thread(runnble);
		thread.start();
		System.out.println("运行结束");
	}
}

运行结束:

运行结束
运行中

在非多继承的情况下,使用继承 Thread 类或者实现 Runnable 接口这俩种方式在取的程序运行的结果上并没有太大的区别。但一旦出现 多继承 的情况,则建议采用实现 Runnable 接口的方式来处理多线程的问题。

3. 共享数据导致的非线程安全问题

共享数据的情况就是多个线程可以访问同一个变量,如在实现投票功能的软件时,多个线程同时处理同一个人的票数。

举个栗子:

public class MyThread extends Thread{

	private int count = 5;
	
	@Override
	public void run(){
		count --
		System.out.println("由" + this.currentThread().getName() + "计算",count=" + count);
	}
}
public class Run{
	
	public static void main(String[] args){

		MyThread myThread = new Mythread();
		
		Thread a = new Thread(myThread,"A");
		Thread b = new Thread(myThread,"B");
		Thread c = new Thread(myThread,"C");
		Thread d = new Thread(myThread,"D");
		Thread e = new Thread(myThread,"E");

		a.start();
		b.start();
		c.start();
		d.start();
		e.start();
	}
}

运行结果:

由 A 计算,count = 3
由 B 计算,count = 3
由 C 计算,count = 2
由 D 计算,count = 1
由 E 计算,count = 0

发现 A ,B 都计算输出了3。发生了共享数据的线程安全问题。

为什么会出现这样问题呢?是因为在 JVM 中 count - - 的操作要分解如下三个操作:

  1. 取的原有 count 值。
  2. 计算 count - 1。
  3. 对 count 重新赋值。

i - - 也会造成出现非线程安全的问题。

解决办法: 可以在 run() 方法钱加 synchronized 关键字即可解决。是多个线程在执行 run() 方法是,以排队的方式进行处理。一个线程在调用 run() 方法时,需要先判断 run() 方法有没有上锁,如果上锁,说明其他线程正在调用 run() 方法,必须等待其他线程调用结束后才可以执行 run() 方法,这也就是实现了排队调用 run() 方法的目的,实现了按顺序对 count 变量减 1 的效果,虽然 i - - 被划为三个步骤,但在执行三个步骤时没有被打断,呈原子性,所以没有了非线程安全问题。

注:方法 run() 是被 JVM 所调用的。

4. 方法 isAlive()

isAlive() 方法的功能是判断线程对象是否存活。

栗子:

public class MyThread extends Thread{
	
	@Override
	public void run(){
		
		System.out.println(“run=+ this.isAlive());
	}
}
public class Run{
	
	public static void main(String[] args){

		MyThread a = new Mythread();
		
		System.out.println("begin ==" + a.isAlive());
		e.start();
		System.out.println("end ==" + a.isAlive());
	}
}

运行结果:

begin ==false
end ==true
run=true

5. 方法 sleep(long millis)

sleep() 方法的作用是在指定的毫秒数内让当前 “正在执行的线程” 休眠(暂停执行),这个 “正在执行的线程” 是指
this.currentThread() 放回的线程。

使用:

Thread.sleep(1000);
this.sleep(1000);

注:如果调用 sleep() 方法的所在类不是 Thread.java ,则必须使用:

Thread.sleep(1000);

6. 如何停止线程

调用 interrupt() 方法来停止线程,但 interrupt() 方法的使用效果并不像 for + break 语句那样,可以马上停止循环。该方法仅仅是在当前线程中打了一个停止标记,并不是真正的停止线程。

1. 使用退出标志使得线程正常退出

  1. this.interrupted() :测试当前线程是否是中断状态,执行后具有清除状态标志值为false的功能。当调用 interrup() 方法后,interrupted() 调用第一次 为 true,后面再调就是false。
  2. this.isInterrupted():测试所在 Thread 线程对象是否已经是中断状态,不清除状态标志。当调用 interrup() 方法后,isInterrupted() 后面调用都是 true。

线程执行任务的时候可以通过 interrupted() 判断 是否中断的状态来终止线程,可以使用 return ,或者方法里只是一个循环的话用break,或者 抛出异常的方法来结束线程。

2. interrupt() 和 sleep()

  • 在 sleep 状态下调用 interrupt() 方法会出现异常。
  • 调用了 interrupt() 给线程打了中断标记,再执行 sleep() 方法也会出现异常。

6. 线程的暂停与恢复

JDK并发包里提供的 LockSuppot 类可实现线程的暂停与恢复:

  • LockSuppot.park():将线程暂停。
  • LockSuppot.uppark():将线程恢复。

7. 方法 yield()

yield() 方法的作用是放弃当前的 CPU 资源,让其他任务去占用 CPU 执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。

8. 线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的 CPU 的资源较多,即 CPU 优先执行优先级较高的线程对象的任务,其实就是让高优先级的线程获得更多的 CPU 时间片。CPU 会尽量将执行资源让给优先级比较高的线程,也就是优先级更高的线程并不一定先执行完,因为线程的优先级还具有随机性,不确定性。

使用 setPriority() 方法设置线程的优先级。

在 Java 中线程的优先级分为 10 个等级,即 1~ 10 ,如果小于 1 或大于 10 ,则 JDK 会抛出异常 throw new IllegalArgumentException()。
JDK 使用三个常量来预定义优先级的值,代码如下:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

在 Java 中,线程的优先级具有继承性,比如 A 线程启动了 B 线程,则 B线程的优先级与 A 的优先级是一样的。

9. 守护线程

Java 中有两种线程,一种是用户线程,也称非守护线程=;另一种就是守护线程。

什么是守护线程?

守护线程是一种特殊的线程,当进程中不存在非守护线程时,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要,自动销毁。即最后一个用户线程销毁了,守护线程会退出,进程也随之结束。

使用 setDaemon(true); 来设置为守护线程。
举个栗子:

public class MyThread extends Thread{
	
	private int i = 0;
	
	@Override
	public void run(){

		try{
			while(true){
				i ++;
				System.out.println("i=" + i);
				Thread.sleep(1000)
				}
			}
	}
}
public class Run{

	public static void main(String[] args){

		MyThread myThread = new MyThread();
		myThread.setDaemon(true);
		myThread.start();
		Thread.sleep(5000);
		System.out.println("我离开thread对象也不再打印了,也就是停止了!");
	}
}

运行结果:

i=1
i=2
i=3
i=4
i=5
i=6
我离开thread对象也不再打印了,也就是停止了!

10. 线程的生命周期

在这里插入图片描述
线程的生命周期大体可以分为五个主要的阶段。

  1. NEW:新建。当我们使用关键字 new 创建一个 Thread 对象时,此时它就是 NEW 状态。
  2. RUNNABLE:就绪。线程对象调用 start 方法,但是并没有立即执行(没有 CPU 调度),这个状态就是 RUNNABLE 状态。由于存在 RUNNING 状态,所以不会直接进入 BLOCKED 状态和 TERMINATED 状态。 RUNNABLE 的线程只能意外终止或者进入 RUNNING 状态。
  3. RUNNING:运行。一旦 CPU 调度 ,执行了,此时的状态就是 RUNNING 状态。
  4. BLOCKED:阻塞。RUNNING 状态调用了 sleep,或者 wait 方法 进入阻塞状态。当然还有其他的方法,如进行某个阻塞的 I/O 操作等。
  5. TERMINATED:销毁。是一个线程的最终状态,在该状态中线程不会切换到其他任何状态,该状态意味着该线程的整个生命周期都结束了。

二、锁的使用

1. Synchronized

关键字 Synchronized 保证了原子性、可见性和有序性。

用 Synchronized 声明的方法所在类的对象就是锁。在 Java 中没有 ‘锁方法’ 的概念,锁的是对象。哪个线程先执行带 Synchronized 关键字的方法,哪个线程就持有该方法所属对象作为锁,其他线程只能等待,前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,也就是每个线程访问自己所属的业务对象,则 JVM 会创建多个锁,不存在锁争抢的情况。

1. Synchronized 方法将对象作为锁。

举个例子:

共享对象:

public class MyObject{

	synchronized public void methodA(){

		try(){
			System.out.println("begin methodA threadName=" + Thread.currentThread.getName());
			Thread.sleep(5000);
			System.out.println("end endTime=" + System.currentTimeMillis());
		} catch (InterruptedException e){
			e.printStackTrace();
		}
	}

	synchronized public void methodB(){

		try(){
			System.out.println("begin methodB threadName=" + Thread.currentThread.getName()+" begin time" + System.currentTimeMillis());
			Thread.sleep(5000);
			System.out.println("en");
		} catch (InterruptedException e){
			e.printStackTrace();
		}
	}
}

创建线程 A 与 B

public class ThreadA extends Thread{

	private MyObject object;

	public ThreadA(MyObject object){		
		super();
		this.object = object;
	}

	@Override
	public void run(){
		super.run();
		obejct.methodA();
	}
}
public class ThreadB extends Thread{

	private MyObject object;

	public ThreadB(MyObject object){		
		super();
		this.object = object;
	}

	@Override
	public void run(){
		super.run();
		obejct.methodB();
	}
}

主运行类:

public class Run(){

	public static void main(String[] args){

		MyObject object = new MyObject();
		ThreadA a = new ThreadA(object);
		ThreadB b = new ThreadB(object);
		a.setName("A");
		b.setName("B");

		a.start();
		b.start();		
	}
}

运行结果:

begin methodA threadName=A
end endTime = 1403575081547
begin methodB threadName=B begin time = 1403575081547
end

总结如下:

  1. A 线程现持有 obejct 对象的锁,B线程如果在这是调用 object 对象中的 synchronized 类型的方法,则需等待,也就是同步。
  2. 在方法声明处添加 synchronized 并不是锁方法,而是锁当前类的对象。
  3. 在 Java 语言中,锁就是对象,对象可以映射成锁,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的 synchronized 同步方法。
  4. 如果在 X 对象中使用了 synchronized 关键字声明非静态方法,则 X 对象就被当成锁。

2. Synchronized 锁重入

关键字 synchronized 拥有重入锁的功能,即在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象锁的。在一个 synchronized 方法/块的内部调用本类的其他 synchronized 方法 /this块时,是永远可以得到锁的。

注:在存在父子类继关系时,子类是完全可以通过锁重入调用父类的同步方法的。

3. Synchronized 同步语句块

用 synchronized 关键字声明方法在某些情况下是有弊端的,比如 A 线程调用到同步方法执行了一个长时间的任务,那么 B 线程就要等待比较长的时间了,此时我们可以使用 synchronized 同步语句块来解决,以增加运行效率。

synchronized 方法是将当前对象作为锁,而 synchronized 代码块是将任意对象作为锁,锁可以认为是一个标识,持有这个标识的线程就可以执行被同步的代码了。

举个栗子:

public class doLongTimeTask{

	private String getData1;
	private String getDate2;
	
	public void doLongTimeTask(){

		try{
			System.out.prinltn("begin task");
			Thread.sleep(3000);
			
			String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName=" + Thread.currentThread().getName();

			String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName=" + Thread.currentThread().getName();
			
			synchronized(this){

				getDate1 = privateGetData1;
				etDate12 = privateGetData2;
				System.out.prinltn(getData1);
				System.out.prinltn(getData2);
				System.out.prinltn("end task");
			
			} 
			
		} catch(){
			e.printStackTrace();
		}
	}
}

当一个线程访问 object 对象的一个 synchronized 同步代码块时,另一个线程仍然可以访问该对象中的非 synchronized(this) 同步代码块。

注:在使用 synchronized(this) 同步代码块时需要注意的是,当一个线程放温暖 object 的一个 synchronized(this) 同步代码块时,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码块的访问将被阻塞,说明 synchronized 使用的 “对象监听器” 是一个,即使用的锁时一个。

3. Synchronized 将任意对象作为锁

Java 中还支持将任意对象作为锁,来实现同步的功能。这个任意的对象大多数是实例变量及方法的参数,格式为 synchronized(非 this 对象)。synchronized (非 this 对象 x)同步代码块的执行特性如下:在多个线程争抢相同的非 this 对象 x 的锁时,同一时间只有一个线程可以执行 synchronized (非 this 对象 x)同步代码块中的代码。

优点:

  • 如果一个类中有很多 synchronized 方法,虽然这时能实现同步,但影响运行效率,如果使用同步代码块锁非 this 对象,则 synchronized (非 this)代码块中的程序与同步方法是异步的,因为是两把锁,不与其他锁 this 同步方法争抢 this 锁,可大大提高运行效率。

注:尽量不要用 String 作为所对象, 而改用其他,例如 new Object()实例化一个新的 Object 对象时,它并不放在缓存池中,或者执行 new String()创建不同的字符串对象,形成不同的锁。而我们使用String 常量作为锁的话,即便使用了比如俩个 “AA”,但其实是同一个常量池中的对象,是同一把锁。

4. 静态同步

每一个 *.java 文件都对应一个类(Class)的实例,在内存中是单例的。

synchronized 关键字还可以应用到静态方法上,如果这样写,那是对当前的 *.java 文件对应的 Class 类的对象进行持锁,Class 类的对象是单例的,更具体的说,在静态方法上使用 synchronized 关键字声明同步方法时,是使用当前静态方法所在类对应 Class 类的单例对象作为锁。Class 锁可以对同一个类的所有对象实例起作用,实现同步效果。

同步 synchronized(class)代码块的作用其实和 synchronized 静态方法的作用一样。

2. ReentrantLock 类

1. 使用 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;
	}
}

调用 ReentrantLock 对象的 lock() 方法获取锁,调用unlock() 方法释放锁,这两个方法成对使用。想要实现同步代码,把这些代码放在 lock()方法 和 unlock() 方法之间即可。

不管是在一个方法还是多个方法的环境中,那个线程持有锁,那个线程就执行业务,其他线程只能等待锁被释放时再次争抢锁,抢到了才开始执行业务,运行效果和 synchronized 关键字一样。

2. 方法 await()与 signal()

一个 Lock 对象可以创建多个 Condition 实例,并且线程对象注册在指定的 Condition 中,从而有选择性地进行线程通知,在调度线程上更加灵活。

await() 方法的作用是使当前线程在接到通知或被中断之前,一直处于等待状态。它和 wart()方法的作用一样。
signal()则是唤醒,通知。
singla()则是唤醒全部,通知全部。

使用案例:

public class MyService{

	//创建锁
	private Lock lock = new ReentrantLock();
	
	private Condition condition = lock.newCondition();
	
	public void await(){
		
		//上锁
		lock.lock;
		
		for(int i = 0 ; i < 5 ; i ++){
			//等待
			condition.await();
			System.out.println("ThreadName="+ Thread.currentThread().getName() + ("" + (i + 1)));
		}
		//释放锁
		lock.unlock;
	}
	
	public void signal(){
	
		lock.lock();
		//唤醒
		condition.signal();
	}
	
}

与 Object 类中的 wait(),notify()方法对比:

  1. Object 类中的 wait()方法相当于 Condition 类中的 await() 方法。
  2. Object 类中的 wait(long timeout)方法相当于 Condition 类中的 await(long time,TimeUnit unit)方法。
  3. Object 类中的 notify()方法相当于 Condition 类中的 signal() 方法。
  4. Object 类中的 notifyAll()方法相当于 Condition 类中的 signalAll()方法。

通知唤醒部分线程使用案例:

public class MyService{

	//创建锁
	private Lock lock = new ReentrantLock();
	
	private Condition conditionA = lock.newCondition();
	private Condition conditionB = lock.newCondition();
	
	public void awaitA(){
		
		//上锁
		lock.lock;
		
		for(int i = 0 ; i < 5 ; i ++){
			// conditionA 等待
			conditionA .await();
			System.out.println("ThreadName="+ Thread.currentThread().getName() + ("" + (i + 1)));
		}
		//释放锁
		lock.unlock;
	}
	
	public void awaitB(){
		
		//上锁
		lock.lock;
		
		for(int i = 0 ; i < 5 ; i ++){
			// conditionB  等待
			conditionB .await();
			System.out.println("ThreadName="+ Thread.currentThread().getName() + ("" + (i + 1)));
		}
		//释放锁
		lock.unlock;
	}
	
	public void signalA(){
	
		lock.lock();
		// conditionA 唤醒
		conditionA.signalAll();
	}
	
	public void signalB(){
	
		lock.lock();
		// conditionB 唤醒
		conditionB.signalAll();
	}
}

3. 公平锁与非公平锁

  • 公平锁:公平锁采用先到先得的策略,每次获取锁之前到会检查队列里面有没有排队等待的线程,没有才会尝试获取锁,如果有就将当前线程追加到队列中。
  • 非公平锁:非公平锁采用 “有机会插队” 的策略,一个线程获取锁之前,要先去尝试获取锁,而不是在队列中等待,如果真的获取锁成功,说明线程虽然是后启动的,但先获得了锁,这就是 “作弊插队” 的效果,如果获取锁没有成功,那么将自身追加到队列中进行等待。
// 创建公平锁
private Lock lock = new ReentrantLock(true);

// 创建不公平锁
private Lock lock = new ReentrantLock(false);

4. tryLock()与 tryLock(long timeout,TimeUnit unit)

  • tryLock():作用是嗅探拿锁,如果当前线程发现锁别其他线程持有了,则返回 false,那么程序继续执行后面的代码,而不是呈阻塞等待锁的状态。
  • tryLock(long timeout,TimeUnit unit):作用是嗅探拿锁,如果在指定的时间内持有了锁,则返回 true,如果过程了时间则返回 false 。timeout 参数代表当前线程抢锁的时间。

5. await(long time, TimeUnit unit)

public boolean await(long time, TimeUnit unit) 方法和 public final native void wait(long timeout) 方法一样,具有自动唤醒的功能。

6. 读写锁 ReentrantReadWriteLock 类

ReentrantLock 具有完全互斥排他的特点,同一时间只有一个线程在执行 ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全性,但效率是非常低下的。在 JDK 中提供了一种 读写锁 ReetrantReadWriteLock 类。可以在同时进行读操作时不需要同步执行,提升运行速度,加快运行效率。这俩个类之间没有继承关系。

读写锁表示俩个锁,一个是读操作相关的锁,也叫共享锁另一个是写操作相关的锁,也叫排它锁。读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。说明只要出现写锁,就会出现互斥同步的效果。读操作是指读取实例变量的值,写操作是指向实例变量写入值。

public class Service{
	//创建读写锁
	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	
	public void read(){

		try{
			//获取读锁
			lock.readLock().lock();
			System.out.println("获得读锁"+ Thread.currentThread().getName());
			Thread.sleep(1000);
		} catch(InterruptedException e){
			e.printStackTrace();
		} finally {
			//释放读锁
			lock.readLock().unlock();
		}
	}

	public void write(){

		try{
			//获取写锁
			lock.writeLock().lock();
			System.out.println("获得读锁"+ Thread.currentThread().getName());
			Thread.sleep(1000);
		} catch(InterruptedException e){
			e.printStackTrace();
		} finally {
			//释放写锁
			lock.writeLock().unlock();
		}
	}
		
}

三、volatile 关键字

三个特性:

  1. 可见性:B 线程能马上看见 A 线程更改的数据。
  2. 原子性:指一组操作在执行时不能被打断。
  3. 禁止代码重排

1. 可见性

举个栗子:

public class RunThread extends Thread{

	private boolean isRunning = true;
	
	public boolean isRunning(){
		return isRunning;
	}
	
	public void setRunning(boolean isRunning){
		this.isRunning = isRunning;
	}
	
	@Override
	public void run(){
		System.out.println("进入run了");
		while(isRunning == true){
			
		}
		System.out.prinln("线程被停止了");
	}
}
public class Run{
	
	public static void main(String[] args){
		try{
			RunThread thread = new RunThread();
			thread.start();
			Thread.sleep(1000);
			thread.setRunning(false);
			System.out.println("已经赋值为false");
		} catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}

程序可能导致死循环,是什么原因呢?

  • 程序启动时,因为变量 private boolean isRunning = true 分别存储在公共内存及线程的私有内存中,线程运行后再线程的私有内存中取得 isRunning 的值一直是 true,而代码 “thread.setRunning(false)” 虽然被执行,却是将公共内存中的 isRunning 变量改成 false,操作的是两块内存地址中的数据,所以一直处于死循环的状态。
  • 这个问题其实就是私有内存和公共内存中的值不同步造成的,可以通过使用 volatile 关键字来解决。volatile 的主要作用就是当线程访问 isRunning 变量时,强制地从公共内存中取值再同步到私有的内存中。

所以 volatile 关键字增加了实例变量在多个线程之间的可见性。

2. 原子性

volatile 的原子性体现在赋值原子性。

volatile 关键字最致命的缺点是不支持运算原子性。也就是多个线程对用 volatile 修饰的变量 i 执行 i++/i- - 操作时, i++/i- - 操作还是会被分成三步,造成线程安全问题。

i ++ 的操作步骤分解如下:

  1. 从内存中取出 i 的值。
  2. 计算 i 的值。
  3. 将 i 的值写到内存中。

我们可以使用 Atomic 原子类进行 i ++ 操作实现原子性。

private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

3.禁止代码重排

什么是重排序?

  • 在 Java 程序运行时,JIT(Just-In-Time Compiler,即时编译器) 为了优化程序的运行,可以动态地改变程序代码运行的顺序。
    比如有如下代码:
A 代码 - 重消耗
B 代码 - 轻消耗
C 代码 - 重消耗
D 代码 - 轻消耗

在多线程的环境中, JIT 有可能进行代码重排,重排后的代码顺序如下:

B 代码 - 轻消耗
D 代码 - 轻消耗
A 代码 - 重消耗
C 代码 - 重消耗

这样做的主要原因是在 CPU 流水线中这 4 个指令是同事执行的,轻耗时的代码很大程度上会先执行完,以让出 CPU 流水线资源供其他指令使用,所以代码重排是为了追求更高的程序运行效率。

重排发生在没有依赖关系时,比如上面的 A B C D 代码,B C D 不依赖 A 的结果,C D 不依赖 A B 的结果,D 不依赖 A B C 的结果时就会发生重排序,如果发生依赖则代码不会重排序。

而 volatile 关键字可以禁止代码重排序,比如有如下代码:

A 代码 - 重消耗
B 代码 - 轻消耗
volatile Z 变量的操作
C 代码 - 重消耗
D 代码 - 轻消耗

那么会有四种情况发生:

  1. A B 可以重排序
  2. C D 可以重排序
  3. A B 不可重排到 Z 后面
  4. C D 不可以重排到 Z 前面

也就是说,变量 Z 是一道屏障,是一堵墙,Z 变量之前或之后的代码不可以跨越 Z 变量,synchronize 关键字也具有相同的特性。

总结 volatile 和 synchronized 关键字的使用场景如下:

  • 当想实现一个变量的值被更改,而其他的线程可以取到最新的值时,就要对变量使用volatile。
  • 如果多个线程对同一个对象中的同一个实例变量进行写操作,为了避免出现非线程安全问题,就要使用 synchronize。

四、线程间通信

1. wait/notify 机制的原理

wait(): 是 Object 类的方法。作用是使当前执行 wait() 方法进行等待,在 wait() 所在的代码进行暂停执行,并释放锁,直到接到通知或被中断为之。在调用 wait() 之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait() 方法。通过通知机制使某个线程继续执行 wait() 方法后面的代码时,对线程的选择是按着执行 wait() 方法的顺序确定的,并需要重新获得锁。如果调用 wait() 时没有持有适当的锁,则抛出 IllegalMonitorStateException。

notify(): 也要在同步方法或同步代码块中调用,即在调用前线程必须获得锁,如果调用 notif() 时没有持有适当的锁,也会抛出 IllegalMonitorStateException。该方法用来通知那些可能 等待该锁的其他线程,如果有多个线程等待,则按着执行 wait() 方法的顺序对呈等待状态的线程发出 1 次通知,并使得那个线程重新获取锁。值得注意的是,执行 notify() 方法后,当前线程不会立马释放锁,呈等待的线程也不会立马获得锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步区域后,当前线程才会释放锁,而呈等待状态所在的线程才可以获得该对象锁。被唤醒的线程呈就绪状态。

notifyAll():方法 notifyAll() 会按照执行 wait() 方法的倒叙依次对其他线程进行唤醒。

2. 方法 join 的使用

方法 join()的作用是使所属的线程对象 x 正常执行 run()方法中的任务,而使当前线程 z 进行无限期的阻塞,等待线程 x 销毁后在继续执行线程 z 后面的代码。

join() 方法内部使用 wait() 方法进行等待,会释放锁。那么释放谁的锁呢?

例如:

Thread thread = new Thread();
thread.start();
thread.join();

则释放的是 thread 这个对象锁。

方法 join(long): 方法 x.join(long) 中的参数用于设定最长的等待时间,当线程小于 long 时间销毁或 long 时间到达并且重新获得了锁时,当前线程会继续向后运行。如果没有重新获得锁,线程会一直尝试,知道获得锁为止。

3. 类 ThreadLocal 的使用

类 ThreadLocal 主要的作用就是将数据放入当前线程对象中的 Map 里,这个 Map 是 Thread 类的实例变量。类 ThreadLocal 自己不管理也不存储任何数据,他只是数据和 Map之间的中介和桥梁,通过 ThreadLocal 将数据放入 Map 中。

Map 中的 key 存储的是 ThreadLocal 对象, value 就是存储的值,ThreadLocal 和 值之间是一对一的关系,一个 ThreadLocal 对象只能关联一个值。每个线程中 Map 的值只对当前线程可见,其他线程不可以访问当前线程对象中的 Map 的值。

使用案例:

public class Run{

	public static ThreadLocal t1 = new ThreadLocal();

	public static void main(String[] args){
		
		if(t1.get() == null){
			System.out.println("从未放过值");
			t1.set("我的值");
		}
		System.out.println(t1.get());
		System.out.println(t1.get());

	}
}

输出:

从未放过值
我的值
我的值

可以看出第一次调用 t1 对象的get()方法返回的值是null。调用 set()方法并赋值后,可以顺利取出值并打印到控制台上。类 ThreadLocal 解决的是变量在不同线程中的隔离性,也就是不同的线程拥有自己的值,不同线程中的值是可以通过 ThreadLocal 类进行保存的。

如何解决第一次 get 返回 null 值的问题的?

可以继承 ThreadLocal 类重写 initialValue() 方法;

public class MyThreadLocal extends ThreadLocal{
	
	@Override
	public Object initialValue(){
		return "我是默认值 第一次 get 不再是 null";
	}
}

还需要注意的是 在我们使用完 ThreadLocal 完之后,我们还应该将里面存储的值删除掉,使用 remove()方法。

为什么要手动删除呢,因为 Map 里存储的 key(ThreadLocal 类)是弱引用的对象(只要垃圾回收器扫描时发现弱引用的对象,则不管内存是否足够,都会回收弱引用对象,也就是执行 gc 操作,ThreadLocal 就会立即释放),会 执行 gc 进行回收,但是 value 值不会随着 gc 操作而销毁,这可能会出现内存溢出的问题。所以需要手动删除。

用法:

ThreadLocal threadLocal = new ThreadLocal();
threadLocal.remove();

五、定时器

1. schedule(TimerTask tsk,Date Time)

schedule(TimerTask tsk,Date Time):该方法的作用是在指定的日期执行一次某一任务。

使用案例:

public class MyTask extends TimerTask{

	@Override
	public void run(){
		System.out.println("任务执行了,时间为:" + System.currentTimeMills());
	}
}


public class Test1{

	public static void main(String[] args){
		long nowTime = System.currentTimeMills();
		System.out.println(nowTime);
	
		long scheduleTime = (nowTime + 10000);
		System.out.println("计划时间为:" + scheduleTime);
		
		Timer timer = new Timer();
		Thread.sleep(1000);
		timer.schedule(task,new Date(scheduleTime));
		
		Thread.sleep(Integer.MAX_VALUE);
	}
}

运行结果:

当前时间为:1520841532234
计划时间为:1520841542234
任务执行了,时间为:1520841542234

当然定时器执行的过程也是可以终止的,调用方法 **timer.cancel();**当前也可以在 TimerTask 类中执行,就是将当前任务从任务列表中移除。

Timer timer = new Timer();
timer.cancel();
//-------------------
public class MyTask extends TimerTask{

	@Override
	public void run(){
		System.out.println("任务执行了,时间为:" + System.currentTimeMills());
		this.cancel();
	}
}

注:如果执行的时间小于当前时间 则立即执行。

该方法也可以执行多个任务,当要执行多个任务时,则再次调用 timer.schedule(task,new Date(scheduleTime)); 方法即可。值得注意的是,当调用多次时,他是以队列的方式一个一个被顺序执行的。所以可能导致后面的任务执行延后了。

2. schedule(TimerTask tsk,Date firstTime,long period)

该方法的作用是在指定的日期之后按指定的间隔周期无线循环地执行某一任务。

3. schedule(TimerTask task,long delay)

以当前的时间为参考,在此时间基础上延迟指定的毫秒数后执行一次 TimerTask 任务。

4. schedule(TimerTask task,long delay,long period)

以当前的时间为参考,在此时间基础上延迟指定的毫秒数后执行一次 TimerTask 任务,在以某一间隔时间无限次数地执行某一任务。

5. scheduleAtFixedRate(TimerTask task,long delay,long period)

以当前的时间为参考,在此时间基础上延迟指定的毫秒数后执行一次 TimerTask 任务,在以某一间隔时间无限次数地执行某一任务。和 schedule(TimerTask task,long delay,long period)的区别在于 scheduleAtFixedRate 具有追赶性,比如某个任务被延时执行了,后面的任务会推迟还是会执行。但是 schedule 如果出现延迟,中间本应该执行的任务,由于延迟了导致没执行,反而执行了后面的任务,就好像该方法一开始执行,每个任我什么时候执行时定死的,谁的时间被耽误了,谁就不执行了。

六、单例模式与多线程

  • 饿汉式/立即加载:使用类的时候已经将对象创建完毕,常见的实现办法就是 new 实例化。
  • 饿汉式/延迟加载:调用 get()方法是,实例才被创建,常见是实现办法就是在 get()方法中进行 new 实例化。

在多线程环境下,单例模式存在非线程安全性问题,那么如何解决呢?

1. synchronize 方法

由于多个线程可以进入 getInstance 方法,我们只需要对 getInstance() 方法声明 synchronize 关键字即可。

public class MyObject{

	private static MyObject myObject;

	private MyObject(){
	}
	synchronize public static MyObject getInstance(){
		if(myObject == null){
			myObject = new MyObject();
		}
	}
}

缺点:运行效率太低。

2. synchronize 同步代码快

public class MyObject{

	private static MyObject myObject;

	private MyObject(){
	}
	public static MyObject getInstance(){
		synchronize(MyObject.class){
			if(myObject == null){
				myObject = new MyObject();
			}
		}
	}
}

缺点:运行效率还是太低。

3. 利用 DCL 双检查锁机制

public class MyObject{

	private volatile static MyObject myObject;

	private MyObject(){
	}
	public static MyObject getInstance(){
		if(myObject == null){
			synchronize(MyObject.class){
				if(myObject == null){
					myObject = new MyObject();
				}
			}
		}
	}
}

使用 volatile 修改变量 myObject,使该变量在多个线程间可见,另外禁止 myObject = new MyObject()代码重排序。myObject = new MyObject()代码包含 3 个步骤。

  1. memory = allocate(); 分配对象的内存空间
  2. ctorInstance(memory); 初始化对象
  3. myObject = memory(); 设置 instance 指向刚分配的内存地址

JIT 编译器有可能将这三个步骤重排序成:

  1. memory = allocate(); 分配对象的内存空间
  2. myObject = memory(); 设置 instance 指向刚分配的内存地址
  3. ctorInstance(memory); 初始化对象

这时,构造方法虽然还没执行,但 myObject 对象已具有内存地址,即值不是null。
DCL 也是大多数多线程结合单例模式使用的解决方案。

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

public class MyObject{

	private static class MyObjectHander{
		private static MyObject myObject = new MyObject();
	}
	
	private MyObject(){
	}

	public static MyObject getInstance(){
		return MyObjectHandler.myObject;
	}
}

5. 使用 static 代码块

public class MyObject{

	private static MyObject instance = null;
	
	private MyObject(){
	}
	
	static{
		instance = new MyObject();
	}
	
	public static MyObject getInstance(){
		reutnr instance;
	}
}

七、线程池

1. ExecutorService 详解

Executor 接口的定义非常简单,仅有一个简单的任务提交方法,代码如下:

public interface Executor{
	//执行任务,至于该任务以何种方式被执行,就要依赖于具体的实现了
	void execute(Runnable command);
}

ExecutorService 接口继承自 Executor 接口,并且提供了更多用于任务提交和管理的一些方法,比如停止任务的执行等。

在这里插入图片描述

ThreadPoolExecutor 是 ExecutorService 最为重要,最为常用的一个实现之一,我们通常所说的 Java 并发包线程池指的就是 ThreadPoolExecutor。

构造 ThreadPoolExcutor 所需要的参数是比较多的,同事提供了四个构造函数的重载形式,但是最终真正被调用的构造函数是囊括了所有 7 个构造参数的构造函数。代码如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        ...
    }
  • corePoolSize: 用于指定在线程池中维护的核心线程数量,即使当前线程池中的核心线程不工作,核心线程的数量也不会减少。

  • maximumPoolSize:设置线程池中允许的线程数量的最大值。

  • keepAliveTime:当线程池中的线程数量超过核心线程数并且处于空闲时,线程池将回收一部分线程让出系统资源,该参数可用于设置超过 corePoolSize 数量的线程在空闲多长时间后被回收,与 unit 配合使用。

  • TimeUnit : 用于设定 keepAliveTime 的时间单位。

  • workQueue: 用于存放已提交至线程池但未被执行的任务。

  • ThreadFactory :用于创建线程的工厂,开发者可以通过自定义 ThreadFactory 来创建线程,比如,根据业务名为线程命名,设置线程优先级、设置线程是否为守护线程等。

  • RejectedExecutionHandler :当任务数量超过阻塞队列边界时,这个时候线程池就会拒绝新增的任务,该参数主要的作用用于设置拒绝策略。

  • 一般最大线程数和核心线程数设置一样大。更具系统CPU进行设置。

    private final int POOL_SEIZE = 4;
    // Runtime.getRuntime().availableProcessors() 返回当前系统的CPU的数目
    private final int CORE_POOL_SIZE= Runtime.getRuntime().availableProcessors()*POOL_SIZE;
    private final int MAXIMUM_POOL_SIZE= Runtime.getRuntime().availableProcessors()*POOL_SIZE;;
    

线程池被成功构造后,其内部的运行线程并不会立即被创建,ThreadPoolExcutor 的核心线程会将采用一种 Lazy (懒)的方式来创建并且运行,当首次调用执行方法(execute)时才会创建,并且运行线程。

ThreadPoolExecutor 构造方法的参数比较多,但他们之间有什么关系呢?

  1. 线程池核心线程数量大于 0 ,并且首次提交任务时,线程池会立即创建线程执行该任务,并且该任务不会被存入任务队列中。
  2. 当线程池中的活跃(工作)线程大于等于核心线程数量并且任务队列未满时,任务队列中的任务不会立即执行,而是等待工作线程空闲时轮询任务队列以获取任务。
  3. 当核心线程数为 2 且都处以工作中,这时候又进来俩个任务,即便最大线程数为4,也不会创建新的线程去执行任务,而是放入任务队列中。若任务队列已满,且正在工作的线程数小于最大线程数,这时候才会去创建新的线程去执行。
  4. 当工作线程已满,且任务队列也已满,又新来了任务,则会执行拒绝策略。
  5. 线程池中的空闲线程,且空闲时长达到 keepAliveTime,线程会被线程池回收,且保留 corePoolSize (核心线程个数)的数量。当然也可以设置全部回收,代码如下。
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
													4,
													30,
													TimeUnit.SECONDS
													new ArrayBlockingQueue<>(10),
													Executors.defaultThreadFactory(),
													new ThreadPoolExecutor.DiscardPolicy());

//全部回收													
executor.allowCoreThreadTimeOut(true);

线程池用完后也需要进行关闭,防止资源的浪费,如何关闭线程呢?

2. 拒绝策略

ThreadPoolExecutor 提供了四种拒绝策略:

  1. DiscardPolicy:拒绝策略。任务会被直接无视丢弃而等不到执行,因此此策略需要慎重使用。
  2. AbortPolicy:中止策略。在无法受理任务时会抛出拒绝执行异常 RejectedExecutionException(运行时异常)。
  3. DiscardOldestPolicy:丢弃任务队列中最老任务的策略。并不是说最早进入队里中的任务就是最老的,比如,优先级阻塞队列会根据排序规则来决定谁放在表头。
  4. CallerRunsPolicy:调用者线程执行策略。该策略不会导致新任务的丢失,但是任务会在当前线程中被阻塞的运行,就是说任务不会由线程池中的线程执行。

3. 线程池的关闭

线程池用完后也需要进行关闭,防止资源的浪费,如何关闭线程呢?

  1. 有序关闭(shutdown)。

shutdown 是一种有序关闭 ExecutorService 的方式。当方法提交后,新提交的任务会被拒绝,但是正在执行的任务,以及已经提交在队列中任务,还是会被执行,当所有的提交任务都执行完成之后线程池中的工作线程才会销毁进而达到线程池最终被关闭的目的。值得注意的是:该方法是立即返回方法,他并不会阻塞等待所有的任务处理结束以及线程池最终的关闭。因此,为了确保线程池被关闭之后才继续执行后面的操作,可以配合等待方法 awaitTermination,作用就是使当前线程进入阻塞等待线程池的关闭结束后在进行下一步动作。

executor.shutdown();
//等待线程池关闭,最多等 10 分钟
executor.awaitTermination(10,TimeUint.MINUTES);
  1. 立即关闭

首先会将线程池状态改为 shutdown 状态,然后将未执行的任务挂起并从任务队列中排干,其次会尝试中断正在处理任务工作线程。最后返回未被执行的任务,当然,新提交的任务也会被拒绝。

//执行立即关闭操作,返回值为 未被执行的任务
List<Runnable> remainingRunnable = executor.shutdownNow();
  1. 组合关闭

通常情况下,为了确保线程池被尽可能的关闭,我们会采取两种关闭线程池的组合方法,以尽可能确保正在运行的任务被正常执行的同时又能提高线程池被关闭的成功率。
该方法也是比较推崇的方式,并且在官方文档中被称之为 关闭线程池模式。

void shutdownAndAwaitTermination(ExecutorService executor, long timeout, TimeUnit unit){
	
	// 首先执行 立即关闭方法
	executor.shutdown();
	
	try{
		//如果在指定时间内线程池仍未被关闭
		if(!executor.awaitTermination(timeout,unit)){
			//则执行立即关闭方法,排干任务队列中的任务
			executor.shutdownNow();
			
			//如果线程池中的工作线程正在执行一个很好使且不可被中断的方法,则中断失败
			if(!executor.awaitTermination(timeout,unit)){
				//未被正常中止,可打印相关日志
			}
		}
		
	}catch (InterrunptedException e){
		//如果当前线程被中断,并且捕获到中断信号,则执行立即关闭方法
		executor.shutdownNow();
		//重新抛出中断方法
		Thread.currentThread().interrupt();
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java多线程编程是指在Java语言中使用多个线程来同时执行多个任务,以提高程序的并发性能和响应速度。Java多线程编程PDF是一本介绍Java多线程编程的PDF文档,其中包含了Java多线程编程的基本概念、原理、技术和实践经验。该PDF文档可以帮助读者快速了解Java多线程编程的相关知识,并提供实用的编程示例和案例分析,有助于读者掌握Java多线程编程核心技术和方法。 在Java多线程编程PDF中,读者可以学习到如何创建和启动线程、线程的状态和生命周期、线程间的通信与同步、线程池的使用、并发容器等相关内容。同时,该PDF文档还介绍了Java中的并发包(concurrent package)的使用和实现原理,以及多线程编程中的常见问题和解决方案。 通过学习Java多线程编程PDF,读者可以深入了解Java多线程编程的理论和实践,掌握多线程编程的核心知识和技能,提高自己的并发编程能力,为开发高性能、高并发的Java应用程序打下坚实的基础。同时,对于已经掌握多线程编程知识的读者来说,该PDF文档也能够帮助他们进一步巩固和扩展自己的多线程编程技能,提升自己的编程水平和竞争力。 总之,Java多线程编程PDF是一本全面介绍Java多线程编程的优秀文档,对于Java程序员来说具有很高的参考价值,可以帮助他们在多线程编程领域取得更好的成就。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值