Java并发(一):线程基本点详解

目录

  • JVM内存模型
       1)原子性
       2)可见性
       3)有序性
  • voliate
  • 线程
  • Java线程
  • Synchronized的作用与原理
  • ReentrantLock
  • ThreadLocal

Java内存模型(Java Memory Model,JMM)
       Java内存模型本身只是一种抽象的概念,它声明了一系列规则或规范(主要是声明了主内存与工作内存交互的一些细节),来解决Java在硬件层面上内存访问的差异,也是Java得以跨平台的原因之一。
  从内存模型的角度出发,Java中内存可分为主内存和线程工作内存两种,所有变量都存储在主内存中,而线程工作内存主要存储线程私有数据。 主内存是共享内存区域,所有线程都可以访问,但线程并不能直操作主内存中的变量,线程如果想操作主内中的变量,需要先将变量从主内存中拷贝到自己的工作内存中,操作完成后再将变量刷回主内存,只有当数据刷回主内存后,最新的结果对其它线程才是可见的(对于volitile变量同样如此),也就说线程之间的通信(这里指可见性)都必须通过主内存来完成。比如下图
Java并发(一):线程基本点详解_第1张图片
  注:这里的主内存与工作内存与堆、栈并不是同一层次上的划分,出发角度不一样,前者是为了解决内存访问差异而划分的,后者代表程序运行时内存的划分。但从变量存储的区域上来看,主内存可对应理解成堆,存储对象但不包括存储局部变量及方法参数,而工作内存则可以对应理解成栈,存储线程局部变量及方法参数等。

Java内存模型的特征
  可见性:可见性是指当线程修改共享变量时,其它线程何时可以看到修改后的信息。上面已经说过,只有当数据刷回主内存之后,对其它线程才是可见的。所以这种可见性不一定是及时的,线程A对共享变量的修改会先反映在自己的工作内存中,然后再写回主内存中,如果在修改完但还没有回写的情况下,线程B去获取此时得到的仍然是旧值。针对这种情况,Java提供了volatile关键字,来保证并发修改下变量的可见性,关于volatile稍后说明。

  原子性:表示对共享数据某个操作的原子性,由于整个操作的原子性,所以在保证原子性的同时也保证了可见性。java中可以简单的通过synchronized、lock等来达到,synchronized也稍后说明。

  有序性:出于优化的考虑,JVM有可能会对指令进行重排序,所以指令的执行顺序并不一定等于代码的编写顺序,但JVM保证最终逻辑上的有序性。比如int a = 3; int b = 4; int c = a + b;  执行时JVM并不保证a一定比b先定义,但从逻辑上会保证,c一定在a,b定义之后才会执行。对于被volatile修饰的变量,jvm不会对其进行重排序。

voliate关键字的作用
 
 对volatile变量的操作同样也遵从内存间的交互规范,与普通变量的区别在于:

  • 对volatile变量的修改会保证及时(同时)反映到线程工作内存和主内存中,而且在每次使用volatile变量前线程都会先从主内存中获取最新值。这点就保证了对volatile变量修改的及时可见。
  • 除可见性外,对于volatile变量jvm不会进行指令重排,指令的执行顺序和程序中的顺序严格一致,如volatile int a =3; int b = 4;JVM会保证a在b之前定义。再比如在JDK1.5之前双重check的懒汉式单例也需要用到volatile来确保对象可以正常使用,原因是,对象创建一般会经历内存分配—>初始化—>返回对象引用等一系列操作。但由于对象引用在内存分配之后就已经确定了,与对象是否初始化并没有逻辑上的关系,所以如果指令重排的话,对象就有可能在尚未初始化完成就提前返回了,即内存分配—>返回对象引用—>初始化,使用的时候,就有可能出现问题。
  • volatile虽然保证对变量修改的及时可见,但并不能保证操作的原子性。
/** 并发情况下,及时获取对该值的修改 */  
  volatile boolean isStop = false;

	public void execute(){
		while(isStop){
			...
		}
	}
	
	public void toStop(){
		isStop = true;
	}

线程简介
  
线程可以分为内核线程(KLT)和用户线程(UT)两种,内核线程由操作系统分配,程序一般不直接调用,而是通过调用系统为内核线程提供的接口—轻量级进程(LWP),去调用内核线程,轻量级进程与内核线程是一对一的关系,每个轻量级进程都是一个独立的调度单元。Java线程模型就是基于操作系统原生线程模型来实现的,在window和unix下,可以理解为下图:
Java并发(一):线程基本点详解_第2张图片
  用户线程建立在用户空间的线程库上,创建、销毁、同步、调度等操作都在用户态中完成,不需要内核参与,因此它的操作相对来更快速而且消耗更低,但难点在于由于没有内核线程的支持,需要手动切换、调度等操作,实现起来非常复杂,目前基于纯用户线程的设计已经很少用了,某些情况下两者会配合使用。  

Java线程简介
线程状态:java线程主要有5种状态new、runnable、blocked、wait/timed_waiting(有限wait)、terminated(执行完毕),通过getState()可获取当前状态。
优先级:优先级从1-10(默认5),新线程优先级默认继承于父线程。 这里的优先级只是Java中的划分,并不对应底层系统的优先级。

守护线程:守护线程在后台运行,当程序中全是守护线程时,jvm会自动退出。在守护线程中应该永远不要去访问如文件、数据库资源这些操作,因为守护线程很可能在任何地方被中断。

sleep(millis):让当前线程休眠一段时间,休眠时间是由参数设定,线程状态由runnable转为timed_waiting,休眠期间释放cpu资源但不释放监视器。在时间到达或线程被中断前,该线程都不会再被调起。在waiting期间如果该线程被任何其它线程中断,则该方法抛出异常,并且清除中断标记。

wait()/wait(millis)、notify()/notifyAll():线程状态由runnable转为waiting/timed_waiting,与sleep()的区别在于它等待时间到达或其它线程notify,且在waiting期间释放cpu资源释放锁,同样受线程中断影响。这两组方法位于根类Object中,两者配合实现一种等待—唤醒的线程协作方式,常用于生产—消费者模式下。

join()/join(millis):等待被调用线程中止,当前线程状态由runnable转为waiting/timed_waiting,所以waiting过程中同样受中断影响。如下:f2等待f1执行完成,main线程又等待f2执行完成。

public static void main(String[] args) throws InterruptedException {
		Thread f1 = new Thread(()->{
			try {
				Thread.sleep(10000);
				System.out.println(Thread.currentThread().getName());
			} catch (InterruptedException e) {}
		});
		
		
		Thread f2 = new Thread(()->{
			try {
				f1.join();
				System.out.println("f1[end], f2[start]");
			} catch (InterruptedException e) {}
		});
		
		f1.start();
		f2.start();
		
		f2.join();
		System.out.println("over");
	}

 yield():使正在运行的线程释放cpu资源重新回到可执行状态,线程状态由runnable(正在执行)到 runnable(可执行),所以线程有可能马上又被执行,由于不会进入wait状态,所以yield()方法并不受线程中断的影响。此外yield()方法只能使同优先级或者高优先级的线程得到运行的机会。

interrupt():中断线程,本身只是设置线程的中断标记而已,但中断会使处于waiting期间的线程抛出一个中断异常InterruptedException,原因是由于线程处于waiting期间,无法被调起执行,不能自己检测中断,所以JVM会通过抛出异常的方式来唤醒线程。通过中断/处理中断异常/检测中断状态也是一种比较常见的线程间通信的协作方式。

stop():
终止线程会释放它已经锁定的所有监视器,很容易造成的问题就是数据不一致性。这就相当于一个DB事务中途退出,对于DB而言事务中途退出会导致事务全部回滚,这没什么问题,但在Java程序中,原子性操作的中途退出并没有回滚一说,而是会产生并发问题,所以数据不一致性的问题就很可能产生。即使要终止线程也可以通过上方说的中断方式,来安全退出。

线程挂起suspend()与恢复resume():这两个方法在Java中被标记为过时的不再使用的方法,原因是因为通过suspend()挂起的线程,不会释放锁,直到在其它线程中调用resume()恢复,且继续运行完成才会释放锁。因此有可能造成两种情况:一就是如果调用resume()的线程也需要获取相同锁,由于原线程并未释放,那么就会造成死锁。二如果resume()在suspend()之前被执行,则原线程永远也无法被恢复。与wait()/notify()的区别在于情况一,wait会释放锁所以不会造成死锁情况,情况二的话wait也不可避免。

	public void testDeadlock() throws InterruptedException{
		final Object lock = new Object();
		Thread t = new Thread(()->{
			synchronized (lock) {	//lock
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {}
				System.out.println("----t.running");
			}
		});
		
		Thread suspend = new Thread(()->{
			t.suspend();
			System.out.println("----t.suspend");
		});
		
		
		Thread resume = new Thread(()->{
			synchronized (lock) {
				t.resume();
				System.out.println("----t.resume");
			}
		});
		
		t.start();
		Thread.sleep(100);
		suspend.start();		//suspend
		Thread.sleep(100);
		resume.start();			//resume
		
		t.join();
		suspend.join();
		resume.join();
		
		System.out.println("----over");
	}

线程异常处理:除了对线程内部的代码进行try...catch外,也可以设置异常处理程序

@Test
	public void testUncaughtExceptionHandler() throws InterruptedException{
		Thread th = new Thread(()->{
			int i = 1/0;
		});
		th.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
			@Override
			public void uncaughtException(Thread t, Throwable e) {
				System.out.println("由于没有捕获异常,所以执行自定义的处理方式" + e.getMessage());
			}
		});
		th.start();
	}

Synchronized块的作用与原理
  synchronized块可以保证操作的原子性和可见性,synchronized底层通过监视器(或内置锁)实现,监视器在jvm中的模型可分为:入口区、持有者、等待区三部分。

Java并发(一):线程基本点详解_第3张图片Java并发(一):线程基本点详解_第4张图片 

  synchronized块的进入和退出分别对应指令monitorenter、monitorexit,且synchronized同步块对同一线程是可重入的,重入通过为内置锁关联一个计数器和持有者线程来实现,计数器为0时即表示监视器未被任何占用。当某线程请求获取到一个未被占用的锁时,JVM将记录锁的持有者,并计数+1,如果同一线程再次请求获取这个锁,计数器依次+1;持有线程每退出一个监视块时,计数器-1,当计数器为0时,则会释放锁。即每执行一个monitorenter对应计数+1,monitorexit对应计数-1。

  • 当一个新的线程想要获取该监听器,但该监视器已被其它线程占用时,新线程将会进入入口区。
  • 持有者代表当前持有监视器的线程,持有者线程可以多次获取监视器而不会被自己阻塞。
  • 等待区是指已进入监视区域但中途由于某种原因(比如等待I/O操作、等待队列消费或生产)而放弃持有监视器的线程。 

ReentrantLock重入锁
  reentrantLock与synchronized很相似,在JDK1.5之前,前者性能更好,1.6起两者性能就差不多了,甚至在往后的JVM会更倾向于synchronized进行改进,所以建议在用synchronized能解决的情况下就使用synchronized。
  目前来看,两者主要是表示形式及功能丰富上存在一些差异(reentrantLock以api的方式进行调用,lock()、unlock()通常使用配合 try...finally使用,后者以原生层面上的互斥锁表现),相对原生的synchronized,reentrantLock提供了一些更高级的功能,如获取可中断,设置非/公平锁、条件等(synchronized 是一个非公平性锁)。

如下是JDK中自带的一个经典使用示例(生产者-消费者模式)

  • 获取可中断:指在获取锁的过程中,可以响应中断操作。如lockInterruptibly()、 tryLock(long timeout,TimeUnit unit)
  • 公平锁:通过new ReentrantLock(true)设置,多线程并发情况下,公平锁倾向于优先将锁分配给等待时间最长的线程。但对于无参的tryLock()公平锁不起作用,只要当前锁可用就被进行分配,而不管其它线程的等待。
  • 多个条件Condition :Condition配合lock一起使用,来实现多条件的锁与唤醒操作。其实它就相当于Object中的wait/notify,只不过使用wait()/notify()时要求当前线程必须先持有对象锁,而condition的使用则没有这个限制,所以更加灵活。
 final Lock lock = new ReentrantLock();
		   final Condition notFull  = lock.newCondition(); 
		   final Condition notEmpty = lock.newCondition(); 

		   final Object[] items = new Object[100];
		   int putptr, takeptr, count;

		   public void put(Object x) throws InterruptedException {
		     lock.lock();
		     try {
		       while (count == items.length) 
		         notFull.await();
		       items[putptr] = x; 
		       if (++putptr == items.length) putptr = 0;
		       ++count;
		       notEmpty.signal();
		     } finally {
		       lock.unlock();
		     }
		   }

		   public Object take() throws InterruptedException {
		     lock.lock();
		     try {
		       while (count == 0) 
		         notEmpty.await();
		       Object x = items[takeptr]; 
		       if (++takeptr == items.length) takeptr = 0;
		       --count;
		       notFull.signal();
		       return x;
		     } finally {
		       lock.unlock();
		     }
		   } 

 

你可能感兴趣的:(Java基础)