Java多线程

175 阅读9分钟

程序:为完成特定任务、用某种语言编写的一组指令集合。即一段静态代码、静态对象。

进程:程序的一次执行过程,或是正在运行的一个程序。是一个动态过程,有它自身的产生、存在、消亡的过程。--生命周期。

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。

多进程vs多线程

体现在程序运行上:

多进程启动多个java.exe并发执行;多线程启动一个java.exe并发执行。(多核CPU才可实现并行执行)

多线程的创建和启动

1. 继承java.lang.Thread类,重写run方法,创建子类对象(即创建新线程),调用Thread类中的start方法启动新线程。

public class Thread1 extends Thread{
	public void run()
	{
		System.out.println("hello");
	}
	public static void main(String[] a)
	{
		new Thread1().start();
	}
}

2. 实现java.lang.Runnable接口,实现run方法,在实例化Thread类时将实现了接口的类对象作为参数传递(即创建新线程),调用Thread类中的start方法启动新线程。

public class Thread2 implements Runnable{
	public void run()
	{
		System.out.println("hello");
	}
	public static void main(String[] a)
	{
		new Thread(new Thread2()).start();
	}
}

Theard vs Runnable

继承Thread占据了继承的名额;

Thread也是实现了Runnable接口的类;

Runnale更易于资源共享;

Runable启动需要Thread类的支持。

3. 实现Callable接口,实现call方法;创建服务,通过服务调用submit()方法开始执行线程;有返回值,通过服务调用get()方法获取返回值;有异常处理。

//部分代码举例
		TestCallable t1=new TestCallable("...");
		TestCallable t2=new TestCallable("...");
		//创建执行服务
		ExecutorService ser=Executors.newFixedThreadPool(2);
		//提交执行
		Future<Boolean> r1=ser.submit(t1);
		Future<Boolean> r2=ser.submit(t2);
		//获取结果
		boolean rs1=r1.get();
		boolean rs2=r2.get();
		//关闭服务
		ser.shutdownNow();

关于启动线程的注意点:

  • 不使用start方法启动线程而直接调用run方法,程序将不会以多线程方式执行,而是串行执行。
  • 同一个线程,只执行第一次start方法,不能多次start。
  • 多个线程启动,CPU调度其执行的顺序是不确定的。
  • 程序终止是指所有线程都终止运行(run方法结束)。

4.线程池

三个类的底层,都是 ThreadPoolExecutor 类实现: ThreadPoolExecutor构造方法的七个参数: 线程池底层工作原理: 自己创建线程池

多线程的信息共享

  • Thread子类的static变量

  • 同一个Runnable类对象的成员变量

信息共享存在的问题:

  1. 工作缓存副本的存在

解决方法:采用volatile关键字修饰变量,保证不同线程对共享变量操作时的可见性。

  1. 关键步骤缺乏加锁限制

解决方法:采用互斥、同步机制。

线程优先级

MIN_PRIORITY=1

NORM_PRIORITY=5(默认)

MAX_PRIORITY=10

优先级低意味着获得调度的概率低,但不对应调用次序,具体调用次序还是看CPU调度。

多线程的状态

线程休眠 sleep

sleep(t)指定当前线程阻塞的毫秒数;

sleep存在InterruptedException异常;

sleep时间到达后线程进入就绪状态;

每一个对象都有一个锁,sleep不会释放锁。

try {
		Thread.sleep(200);//ms
	} catch (InterruptedException e) {
		e.printStackTrace();
	}

线程礼让 yield

yield 让当前正在运行的线程进入就绪状态,cpu重新调度,礼让不一定成功。

public class TestYield {
	public static void main(String[] args) {
		MyYield t=new MyYield();
		new Thread(t,"A").start();
		new Thread(t,"B").start();
	}
}

class MyYield implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"执行。。。");
		Thread.yield();
		System.out.println(Thread.currentThread().getName()+"结束。。。");
	}
}

线程强制执行 join

线程执行join()方法后,其它线程阻塞,待此线程执行完毕,再重新调度。

public class TestJoin implements Runnable{
	@Override
	public void run() {
		for(int i=1;i<=100;i++) {
			System.out.println("插队线程 --"+i);
		}
	}
	public static void main(String[] args) {
		TestJoin t=new TestJoin();
		Thread thread=new Thread(t);
		thread.start();
		for(int i=1;i<=100;i++) {
			System.out.println("主线程 --"+i);
			if(i==20) {
				try {
					thread.join();//插队
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

多线程死锁

指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。每个线程都持有别人需要的锁又在等待别的死锁线程拥有的锁。

死锁四个必要条件:

互斥条件

请求与保持条件

不剥夺条件

循环等待条件

死锁的避免与预防

用户线程和守护线程

新创建的线程默认是用户线程,jvm需要等待所有用户线程执行完毕才会停止工作。

守护线程是为用户线程服务的,用户线程执行完毕后jvm不用等待守护线程执行完毕。

守护线程举例:操作日志、监控内存、垃圾回收的线程

Thread t1=new Thread(new Thread1());
t1.setDaemon(true);//设置t1为守护线程

Java并发编程

1.线程组ThreadGroup

优点:

  • 相当于线程的集合,可以有效管理多个线程

缺点:

  • 管理线程效率低
  • 任务分配和执行过程高度耦合
  • 重复创建线程、关闭线程,无法重用线程。

2.并发框架Executor

优点:

  • 任务创建和执行过程解耦
  • 线程重复利用
  • 程序员无需关心线程池执行任务过程

主要类:ExecutorService,ThreadPoolExecutor,Future

3.并发框架Fork-Join

Fork:把一个复杂任务进行分拆,大事化小

Join:把分拆任务的结果进行合并

优点:

  • 分解、治理、合并。分治编程,适用于整体任务量不好确定,最小任务可以确定的场合

主要类:ForkJoinPool、RecursiveAction、RecursiveTask

多线程并发控制

Lock

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问资源之前应先获得Lock对象。

  • 乐观锁/悲观锁
  • 可重入锁(ReentrantLock/Synchronized)
  • 公平/非公平锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁
  • 独占锁(写锁)/共享锁(读锁)/互斥锁

synchronized关键字锁的是什么? 同步块 synchronized(Obj){}

Obj 称为同步监视器

Obj可以是任何对象,推荐使用共享资源作为同步代码块的同步监视器

同步方法无需指定同步监视器,默认为this, 即这个对象本身,或是Class(反射)。

可重入锁 最大作用是防止死锁

公平锁 :指多个线程按照申请的顺序来获取锁。

非公平锁 :指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁;在高并发情况下,可能造成优先级反转或者饥饿现象.

并发包中ReentrantLock的创建可以指定构造函数boolean类型参数来得到公平锁或非公平锁,无参默认为非公平锁。

对于ReentrantLock而言,非公平锁优点在于吞吐量比公平锁大。

对于Synchronized而言,是一种非公平锁。

自旋锁 :指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。可以减少线程上下文切换的消耗,缺点是会消耗CPU。

独占锁

指该锁一次只能被一个线程所持有。ReentrantLock和Synchronized都是独占锁。

共享锁

指该锁可被多个线程所持有。

ReentrantReadWriteLock实现了ReadWriteLock接口,其读锁是共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读的高效,读写、写读、写写的过程都是互斥的。

Phaser

Exchanger

Java定时任务

定时执行:固定一个时间点并以某一个周期运行。

简单定时机制(TimerTask、Timer)

Executor+定时器机制

Quartz

Timer执行周期任务,如果中间某次有异常,整个任务终止执行;

Quartz功能更强大,执行周期任务,如果中间某次有异常,不影响下个任务执行。)

Synchronized 关键字(略)

锁的是什么?---Java对象头

ReentrantLock和AQS

ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放加锁,可加多把两两配对的锁。

围绕自旋、CAS、park-unpark实现,锁状态state默认初始值为0。

源码分析:公平锁、无锁且队为空的情况: 调用lock方法,tryAcquire方法先判断当前锁状态,state值为0则继续判断队列是否为空,为空则使用CAS操作将当前线程状态state值由0设为1,此时当前线程获得锁成功,tryAcquire方法返回true,程序继续执行调用lock方法后的代码。

总结:只有单线程或线程交替执行的情况,不会使用到AQS。

非公平锁、无锁且队列为空的情况: 调用lock方法,ReentrantLock的内部类NonfairSync的lock方法会先判断当前锁状态,state值为0,则直接使用CAS操作将当前线程状态state值由0设为1,此时当前线程获得锁成功,CAS操作失败,则调用nonfairTryAcquire方法,再次判断当前锁状态,state值为0,则直接使用CAS操作将当前线程状态state值由0设为1,此时当前线程获得锁成功,程序继续执行调用lock方法后的代码。

可重入分析: 如果当前尝试获取锁的线程和已经获得锁的线程是同一线程,令锁状态值state+1,当前线程获得锁成功返回。

已经有线程持有锁的情况:

此时tryAcquire(arg)方法返回false,不发生短路,继续执行&&后的代码。

先初始化AQS队列:给队列添加一个虚拟节点,即:为队列设置一个thread为空的Node对象作为队列的第一个对象,AQS的head和tail都指向这个对象。 然后当前线程会自旋尝试获得锁,。 两次自旋失败就将当前线程阻塞-park。

{

state==0(锁为自由状态)?且等待队列为空-->可以获得锁,令c=1

c==0?且等待队列不为空-->根据是否公平锁决定谁获得锁(?)

c==1?如果是持有锁的线程-->正常返回

c==1?不是持有锁的 线程-->不能获得锁,阻塞,加入等待队列,等待运行的线程释放锁唤醒线程。

}

//②定义lock锁
	private final ReentrantLock lock=new ReentrantLock();
	@Override
	public void run() {
		while(flag) {
			lock.lock();//加锁(改变了底层属性值的标记)
			try {
				buy();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}finally {
            	//同步代码块有异常,将unlock()写在finally中
				lock.unlock();//释放锁
			}
		}
	}

synchronized与Lock对比 ①synchronized是jvm层面,是Java关键字:

lock是具体类,api层面,jdk5以后。

②synchronized是隐式锁,出了作用域自动释放;

Lock是显式锁(手动开启和关闭)。

③等待是否可中断:synchronized不可中断(除非抛出异常或正常运行完成);

Lock可不中断可中断(1.设置超时方法tryLock(Long timeout,TimeUnit unit);2.lockInterruptibly,放代码块中,调用interrupt方法可中断)。

④加锁是否公平:synchronized非公平锁;

Lock两者都可以,可以设置是否公平锁。

⑤ Lock可以绑定多个条件Condition,用来实现分组唤醒线程。

⑥ Lock只有代码块锁,synchronized有代码块锁和方法锁;

其它:使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多子类)。

优先使用顺序:Lock->同步代码块->同步方法

Volatile 关键字

volatile是一个类型修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

valotile(JVM提供的轻量级同步机制):1,保证可见性;2,不保证原子性;3,禁止指令重排

1.保证可见性:一个线程读写完堆中的值之后,其它使用这个变量值的线程也能马上知道(修改虚拟机栈中变量的值)。

2.不保证原子性,如i++操作,解决方法:AtomicInteger。

指令重排概述:

单例模式 使用volatile关键字修饰instance,禁止指令重排,保证线程安全

CAS 比较并交换

CAS 不加锁,保证一致性,又提高了并发性;

synchronized一次只允许一个对象访问,降低了并发性。

AtomicInteger atomicInteger=new AtomicInteger(5);

//5为期望值,2019为设置的新值。为5则设置成功,否则失败。
atomicInteger.compareAndSet(5,2019);

//Unsafe类+CAS思想-->底层汇编,操作系统保证原子操作
atomicInteger.getAndIncrement();

缺点:

①如果CAS失败,会一直尝试,比较时间长,CPU开销大(do while);

②只能保证一个共享变量的原子操作;

③ABA问题。

解决ABA问题:原子引用--AtomicStampedReference

CountDownLatch

CyclicBarrier

Semaphore

阻塞队列

生产者-消费者交替执行

<生产者-消费者1.0> <生产者-消费者2.0> <生产者-消费者3.0> (阻塞队列版)