JAVA多线程杂学3-2018年10月25日

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/attack_breast/article/details/83385212

如何实现多线程的同步?

在Java 中提供了四种方式来实现同步互斥访问:

synchronized 和Lock和wait()/notify()/notifyAll()方法和CAS。

synchronized 的用法

synchronized(object){}

①当修饰非静态的方法

当synchronized 关键字修饰一个方法的时候,该方法叫做同步方法。Java 中的每个对象都有一个锁(lock),或者叫做监视器(monitor),当一个线程访问某个对象的synchronized 方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized 方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized 方法。

注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。如果一个对象有多个synchronized 方法,某一时刻某个线程已经进入到了某个synchronized 方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized 方法的。(这句话的意思是synchronized锁的是同一个对象时,那么与这个对象有关的所有的synchronized方法都不能访问,因为加锁对象是同一个且只能获取一个锁)

②当修饰静态的方法

当一个synchronized 关键字修饰的方法同时又被static 修饰,之前说过,非静态的同步方法会将对象上锁,但是静态方法不属于对象,而是属于类,它会将这个方法所在的类的Class 对象上锁。一个类不管生成多少个对象,它们所对应的是同一个Class 对象。因此,当线程分别访问同一个类的两个对象的两个static synchronized方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始。

③结论

synchronized 方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized 方法。synchronized 块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内,synchronized 块之外的其他代码是可以被多个线程同时访问到的。

Lock 的用法

使用Lock 必须在try-catch-finally 块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被释放,防止死锁的发生。

wait()\notify()\notifyAll()的用法

在Java 发展史上曾经使用suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。

解决方案可以使用以对象为目标的阻塞,即利用Object 类的wait()和notify()方法实现线程阻塞。

首先,wait、notify 方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;

其次,wait、notify 方法必须在synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify 方法的对象是同一个,如此一来在调用wait 之前当前线程就已经成功获取某对象的锁,执行wait 阻塞后当前线程就将之前获取的对象锁释放。

CAS

它是一种非阻塞的同步方式。具体参见上面的部分。

Lock 和synchronized 的区别和Lock 的优势?

①Lock 是一个接口,而synchronized 是Java 中的关键字,synchronized 是内置的语言实现;

②synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock 时需要在finally 块中释放锁;

③Lock可以让等待锁的线程响应中断(可中断锁),而synchronized却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断(不可中断锁);

④通过Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取了锁,则返回true;否则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。),而synchronized 却无法办到。

⑤Lock 可以提高多个线程进行读操作的效率(读写锁)。

⑥Lock 可以实现公平锁,synchronized 不保证公平性。

在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock 的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

volatile synchronized 区别?

①volatile 是变量修饰符,而synchronized 则作用于代码块或方法。

②volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会对变量加锁,可能会造成线程的阻塞。

③volatile 仅能实现变量的修改可见性,并不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。(synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改刷新到主存中)。

④volatile 标记的变量不会被编译器优化,禁止指令重排序;synchronized 标记的变量可以被编译器优化。

什么场景下可以使用volatile 替换synchronized?

只需要保证共享资源的可见性的时候可以使用volatile 替代,synchronized 保证可操作的原子性,一致性和可见性。

为什么wait(),notify(),notifyAll()等方法都定义在Object 类中?

因为这三个方法都需要定义在同步代码块或同步方法中,这些方法的调用是依赖锁对象的,而同步代码块或同步方法中的锁对象可以是任意对象,那么能被任意对象调用的方法一定定义在Object 类中。

notify()notifyAll()有什么区别?

notify()和notifyAll()都是Object 对象用于通知处在等待该对象的线程的方法。

void notify(): 唤醒一个正在等待该对象的线程,进入就绪队列等待CPU 的调度。

void notifyAll(): 唤醒所有正在等待该对象的线程,进入就绪队列等待CPU 的调度。

两者的最大区别在于:notifyAll 使所有原来在该对象上等待被notify 的线程统统退出wait 的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。notify 他只是选择一个wait 状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify 的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify 语句,即便该对象已经空闲,其他wait 状态等待的线程由于没有得到该对象的通知,继续处在wait 状态,直到这个对象发出一个notify 或notifyAll,它们等待的是被notify 或notifyAll,而不是锁。

java 中的悲观锁和乐观锁?

悲观锁:悲观锁是认为肯定有其他线程来争夺资源,因此不管到底会不会发生争夺,悲观锁总是会先去锁住资源,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。Synchronized 和Lock 都是悲观锁。

乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。就是当去做某个修改或其他操作的时候它认为不会有其他线程来做同样的操作(竞争),这是一种乐观的态度,通常是基于CAS 原子指令来实现的。CAS 通常不会将线程挂起,因此有时性能会好一些。乐观锁的一种实现方式——CAS

实现线程之间的通信?

①Object 类中wait()\notify()\notifyAll()方法。

②用Condition 接口。

③管道实现线程间的通信。

④使用volatile 关键字

如何确保线程安全?

如果多个线程同时运行某段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。Synchronized,Lock,原子类(如atomicinteger 等),同步容器、并发容器、阻塞队列、同步辅助类(比如CountDownLatch, Semaphore,CyclicBarrier)。

多线程中锁的种类

可重入锁

ReentrantLock 和synchronized 都是可重入锁。如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁。

在上面代码段中,执行test 方法需要获得当前对象作为监视器的对象锁,但方法中又调用了test2 的同步方法。如果锁是具有可重入性的话,那么该线程在调用test2 时并不需要再次获得当前对象的锁,可以直接进入test2 方法进行操作。可重入锁最大的作用是避免死锁。如果锁是不具有可重入性的话,那么该线程在调用test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得,那么线程在调用同步方法、含有锁的方法时就会产生死锁。

可中断锁

顾名思义,就是可以响应中断的锁。

在Java 中,synchronized 不是可中断锁,而Lock 是可中断锁。lockInterruptibly()的用法已经体现了Lock 的可中断性。如果某一线程A 正在执行锁中的代码,另一线程B 正在等待获取该锁,可能由于等待时间过长,线程B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中断它,这种就是可中断锁。

公平锁

在Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

读写锁

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock 实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

锁优化

自旋锁

为了让线程等待,让线程执行一个忙循环(自旋)。需要物理机器有一个以上的处理器。自旋等待虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之自旋的线程只会白白消耗处理器资源。自旋次数的默认值是10 次,可以使用参数-XX:PreBlockSpin 来更改。

自适应自旋锁:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁清除

指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行清除(逃逸分析技术:在堆上的所有数据都不会逃逸出去被其它线程访问到,可以把它们当成栈上数据对待)。

锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。

轻量级锁

在代码进入同步块时,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储所对象目前的Mark Word 的拷贝。然后虚拟机将使用CAS 操作尝试将对象的Mark Word 更新为执行Lock Record 的指针。如果成功,那么这个线程就拥有了该对象的锁。如果更新操作失败,虚拟机首先会检查对象的Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,否则说明这个对象已经被其它线程抢占。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

解锁过程:如果对象的Mark Word 仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word 和和线程中复制的Displaced Mark Word 替换回来,如果替换成功,整个过程就完成。如果失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁的依据:对于绝大部分的锁,在整个同步周期内都是不存在竞争的。传统锁(重量级锁)使用操作系统互斥量来实现的。

偏向锁

目的是消除在无竞争情况下的同步原语,进一步提高程序的运行性能。锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,则持有锁的线程将永远不需要再进行同步。

当锁第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01,同时使用CAS 操作把获取到这个锁的线程的ID 记录在对象的Mark Word 之中,如果成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,都可以不进行任何同步操作。

当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。

wait()sleep()的区别

①这两个方法来自不同的类,sleep()来自Thread 类,是静态方法;wait()是Object 类里面的方法,和notify()或者notifyAll()方法配套使用,来实现线程间的通信。

②最主要是sleep 是将当前线程挂起指定的时间,没有释放锁;而wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。

③使用范围:wait,notify 和notifyAll 只能在同步控制方法或者同步控制块里面使用,而sleep 可以在任何地方使用。

特别注意:sleep 和wait 必须捕获异常(Thread.sleep()和Object.wait()都会抛出InterruptedException),notify 和notifyAll 不需要捕获异常。

Java 中interrupted() 和isInterrupted()方法的区别?

二个方法都是判断线程是否停止的方法。

①前者是静态方法,后者是非静态方法。interrupted 是作用于当前正在运行的线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A 线程中去调用B 线程对象的isInterrupted 方法,此时,当前正在运行的线程就是A 线程。)

②前者会将中断状态清除而后者不会。

Java 创建线程之后,直接调用start()方法和run()的区别?

①start()方法来启动线程,并在新线程中运行run()方法,真正实现了多线程运行。这时无需等待run 方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread 类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行,然后通过此Thread 类调用方法run()来完成其运行操作,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程终止。然后CPU 再调度其它线程。

②直接调用run()方法的话,会把run()方法当作普通方法来调用,会在当前线程中执行run()方法,而不会启动新线程来运行run()方法。程序还是要顺序执行,要等待run 方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到多线程的目的。

怎么检测一个线程是否拥有锁?

在java.lang.Thread 中有一个方法叫holdsLock(Object obj),它返回true,如果当且仅当当前线程拥有某个具体对象的锁。

用户线程和守护线程有什么区别?

当我们在Java 程序中创建一个线程,它就被称为用户线程。将一个用户线程设置为守护线程的方法就是在调用start() 方法之前, 调用对象的setDamon(true)方法。一个守护线程是在后台执行并且不会阻止JVM 终止的线程,守护线程的作用是为其他线程的运行提供便利服务。当没有用户线程在运行的时候,JVM 关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。守护线程的一个典型例子就是垃圾回收器。

什么是线程调度器?

线程调度器是一个操作系统服务,它负责为Runnable 状态的线程分配CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

线程的状态

在Java 当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

第一是创建状态。在生成线程对象,并没有调用该对象的start 方法,这是线程处于创建状态。

第二是就绪状态。当调用了线程对象的start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run 函数当中的代码。

第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,wait 等方法都可以导致线程阻塞。

第五是死亡状态。如果一个线程的run 方法执行结束或者异常中断后,该线程就会死亡。对于已经死亡的线程,无法再使用start 方法令其进入就绪。

有三个线程T1T2T3,怎么确保它们按顺序执行?

join()方法

java 程序如何停止一个线程?

建议使用”异常法”来终止线程的继续运行。在想要被中断执行的线程中,调用interrupted()方法,该方法用来检验当前线程是否已经被中断,即该线程是否被打上了中断的标记,并不会使得线程立即停止运行,如果返回true,则抛出异常,停止线程的运行。在线程外,调用interrupt()方法,使得该线程打上中断的标记。

ThreadLocal 的原理

ThreadLocal 相当于一个容器, 用于存放每个线程的局部变量。

ThreadLocal 实例通常来说都是private static 类型的。ThreadLocal 可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是访问不到的,各个线程中访问的是不同的对象。如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。向ThreadLocal 中set 的变量是由Thread 线程对象自身保存的,当用户调用ThreadLocal 对象的set(Object o) 时, 该方法则通过Thread.currentThread() 获取当前线程, 将变量存入线程中的ThreadLocalMap 类的对象内,Map 中元素的键为当前的threadlocal 对象,而值对应线程的变量副本。

 

猜你喜欢

转载自blog.csdn.net/attack_breast/article/details/83385212