java内存模型定义了程序中各种变量的访问规则。其规定所有的变量都存储在主内存(main memory),线程均有自己的工作内存(local memory)。
工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据,操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回内存。
**volatile是Java提供的一种轻量级的同步机制。**Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
1、保证变量对所有线程的可见性(当一条线程修改了变量值,新值对于其他线程来说是立刻可以得知的)
2、禁止指令重排序优化,使用volatile变量进行写操作,其实现源码中lock前缀,相当于一个内存屏障,编译器不会把后面的指令重排到内存屏障之前。
1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
public class TestVolatile{
int a = 1;
boolean status = false;
//状态切换为true
public void changeStatus{
a = 2; //1
status = true; //2
}
//若状态为true,则为running
public void run(){
if(status){ //3
int b = a + 1; //4
System.out.println(b);
}
}
}
class MyThread extends Thread {//线程主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() {//线程的主体方法
for(int x = 0; x < 10 ; x++) {
System.out.println(this.title + "运行,x = " + x);
}
}
}
多线程要执行的功能,都应该在run()方法中进行定义,但是需要说明的是:
在正常情况下,如果要想使用一个类中的方法,那么肯定要产生实例化对象,而后去调用类中提供的方法,但是run()方法不能直接调用的,因为这牵扯到一个操作系统的资源调度问题,所以要想启动多线程必须使用start()方法。
class MyThread extends Thread {//线程主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() {//线程的主体方法
for(int x = 0; x < 10 ; x++) {
System.out.println(this.title + "运行,x = " + x);
}
}
}
class MyThread implements Runnable {//线程主体类
private String title;
public MyThread(String title) {
this.title = title;
}
@Override
public void run() {//线程的主体方法
for(int x = 0; x < 10 ; x++) {
System.out.println(this.title + "运行,x = " + x);
}
}
}
但是此时由于不在继承Thread父类了,那么对于此时的MyThread类中也就不在支持有start()这个继承方法,可是不使用Thread.start()方法是无法进行多线程启动的,那么这个时候就需要去观察一下Thread类所提供的构造方法了。
public class ThreadDemo {
public static void main(String[] args) {
Thread threadA = new Thread(new MyThread("线程A"));
Thread threadB = new Thread(new MyThread("线程B"));
Thread threadC = new Thread(new MyThread("线程C"));
threadA.start();
threadB.start();
threadC.start();
}
}
经过一系列的分析之后可以发现,在多线程的实现过程之中已经有了两种做法:Thread类、Runnable接口,如果从代码的结构本身来讲肯定使用Runnable是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充。
但是从结构上也需要观察Thread与Runnable的联系,打开Thread类的定义:
public class Thread extends Object implements Runnable{}
发现Thread类也是Runnable 接口的子类,那么在之前继承Thread类的时候实际上覆写的还是Runnable的方法。
多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交给Thread类来处理。
在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法。当通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread中target的属性保存,在start()方法执行的时候会调用Thread类中的run方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。
多线程开发的本质实质上是在于多个线程可以进行统一资源的抢占,那么Thread主要描述的是线程,那么资源的描述是通过Runnable完成的。
public interface Callable<V> {
public V call() throws Exception;
}
可以发现Callbale定义的时候可以设置一个泛型,此泛型的类型就是返回数据的类型,这样的的好处是可以避免向下转行所带来的安全隐患。
Callable的实现
class MyThread2 implements Callable<String> {
@Override
public String call() throws Exception {
for ( int x = 0 ; x < 10 ; x ++ ) {
System.out.println("******线程执行,x = " + x);
}
return "线程执行完毕!";
}
}
public class demo {
public static void main(String[] args) throws Exception{
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask).start();
System.out.println("线程返回值:" + futureTask.get());
}
}
面试题:请解释Runnable 与 Callable的区别:
Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的;
java.lang.Runnable 接口之中只提供了一个run()方法,并且没有返回值;
java.util.concurrent.Callable接口提供有call(),可以有返回值;
synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,其原理是通过当前线程持有当前对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,也就保证了线程安全。synchronized也可以保证线程的可见性。synchronized属于隐式锁,锁的持有与释放都是隐式的,我们无需干预。
synchronized最主要的三种应用方式:
修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
public synchronized void show() {
System.out.println("Synchronized修饰的一般方法!");
}
public static void main(String[] args) {
Test0105 test=new Test0105();
test.show();
}
修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
public static synchronized void show() {
System.out.println("Synchronized修饰的静态方法!");
}
修饰代码块,指定加锁对象。对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public void show() {
Object o=new Object();
synchronized (o) {
System.out.println("Synchronized修饰的代码块!");
}
}
Synchronized底层语义原理:
每个对象的对象头都关联着一个monitor对象,当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。每个等待锁的线程都会被封装成ObjectWaiter对象,存放在ObjectMonitor中,ObjectMonitor中有两个队列,_WaitSet和_EntryList,,_owner区域指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示:
对象在内存中的布局
锁级别:偏向锁->轻量级锁->自旋锁->重量级锁:
(1)偏向锁:如果一个线程获得了锁,那么进入偏向模式,当这个线程再次请求锁的时候,无需再做任何同步操作,这样就省去了大量有关锁申请的操作。适用于连续多次都是同一个线程申请相同的锁的场景。
(2)轻量级锁:适用于多个线程交替执行同步块的时候
(3)自旋锁:自旋锁是一种假设在不久将来,当前的线程可以获得锁。因此在轻量级锁升级成为重量级锁之前虚拟机会让当前想要获取锁的线程做几个空循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起。
这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用CPU的时间变长会导致性能急剧下降,因此Java虚拟机内部一般对于自旋锁有一定的次数限制,可能是50或者100次循环后就放弃,直接挂起线程,让出CPU资源。
(4)重量级锁:适用于多个线程同时执行同步代码块的场景。
lock是显式锁,锁的持有与释放都必须由我们手动编写,当前线程使用lock()方法与unlock()对临界区进行包围,其他线程由于无法持有锁将无法进入临界区,直到当前线程释放锁,unlock()操作必须在finally代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁。
(1)synchronized是Java语言的关键字,是隐式锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;Lock是一个接口,是显式锁,必须要用户去手动调用unlock()释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
(2)synchronized在发生异常时,JVM会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象。
(3)Lock锁在使用上比synchronized具有更大的灵活性,synchronized能够完成的功能,Lock锁基本也能完成,同时lock锁还拥有synchronized锁所没有的其他功能。比如:
①Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
②通过Lock可以知道有没有成功获取锁(tryLock),而synchronized却无法办到。
③Lock可以提高多个线程进行读操作的效率(读写锁)。
④Lock接口的实现类ReentrantLock可以添加多个检控条件,如果使用synchronized,则只能有一个,而使用ReentrantLock可以有多个等待队列。
(4)Lock可以实现公平锁,Synchronized不保证公平性。
tryLock()方法是有返回值的,返回值是Boolean类型。它表示的是用来尝试获取锁:成功获取则返回true;获取失败则返回false,这个方法无论如何都会立即返回。不会像synchronized一样,一个线程获取锁之后,其他锁只能等待那个线程释放之后才能有获取锁的机会。
一般情况下的tryLock获取锁匙这样使用的:
//实例化Lock接口对象
Lock lock = ...;
//根据尝试获取锁的值来判断具体执行的代码
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
//当获取锁成功时最后一定要记住finally去关闭锁
lock.unlock(); //释放锁
}
}else {
//else时为未获取锁,则无需去关闭锁
//如果不能获取锁,则直接做其他事情
}
ReentrantLock实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。ReentrantLock本身是一种支持重进入的锁,即该锁可以支持一个线程对资源的重复加锁,同时也支持公平锁和非公平锁。ReentrantLock是基于AQS并发框架实现的。
Lock lock = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.ublock();
}
ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因此诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
AQS框架是J.U.C中实现锁及同步机制的基础,是一个抽象类,主要是维护了一个int类型的state属性和一个非阻塞、先进先出的线程等待队列;其中state是用volatile修饰的,保证线程之间的可见性和控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待;
AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。AQS中只能存在一个同步队列,但可拥有多个等待队列。
队列的入队和出对操作都是无锁操作,基于自旋锁和CAS实现;AQS分为两种模式:独占模式和共享模式,像ReentrantLock是基于独占模式模式实现的,Semaphore,CountDownLatch、CyclicBarrier等是基于共享模式。
J.U.C的同步器主要用于协助线程同步,有以下四种:
(1)闭锁 CountDownLatch:闭锁主要用于让一个主线程等待一组事件发生后继续执行。
(2)栅栏 CyclicBarrier:栅栏主要用于等待其它线程,且会阻塞自己当前线程,所有线程必须同时到达栅栏位置后,才能继续执行;且在所有线程到达栅栏处,可以触发执行另外一个预先设置的线程。
(3)信号量 Semaphore:信号量主要用于控制访问资源的线程个数,常常用于实现资源池,如数据库连接池,线程池。在Semaphore中,acquire方法用于获取资源,有的话,继续执行,没有资源的话将阻塞直到有其它线程调用release方法释放资源;
(4)交换器 Exchanger:交换器主要用于线程之间进行数据交换;当两个线程都到达共同的同步点(都执行到exchanger.exchange的时刻)时,发生数据交换,否则会等待直到其它线程到达;
问题:CyclicBarrier和CountDownLatch的区别:
回答:两者都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
①CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行;
②CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务;
③CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。