1、锁
1.1、乐观锁和悲观锁
悲观锁:是什么意思呐,是这样的,认为自己在使用共享数据的时候一定有其他线程来对数据进行操作(写),因此在获取数据的时候会先加锁,确保数据不会被其他的线程修改。
synchronized关键字和Lock的实现类都是悲观锁,这种方式适合对数据进行写操作比较频烦的场景,先加锁可以保护写数据时数据的正确,先锁定再操作数据!
乐观锁:认为自己再使用数据的时候不会有其他的线程来修改我操作的数据,所以不会添加锁,再java中是使用无锁编程来实现,只是再更新数据的时候去判断,有没有其他的线程来更新数据,如果数据没有被更新,当前的线程将自己想要修改的数据写入,如果数据被更新则根据不同的实现方式执行不同的操作,比如放弃修改,重试抢锁等等。
适合读操作比较多的场景,不加锁的特点能够使其读操作性能大幅度的提升,乐观锁直接去操作同步资源,是一种无锁算法,得之我幸失之我命。乐观锁的实现方式:采用Version版本号机制,CAS算法实现
1.2、对象锁和类锁
public class Lock8Demo {
public static void main(String[] args)//一切程序的入口
{
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"a").start();
//暂停毫秒,保证a线程先启动
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start();
}
}
class Phone //资源类
{
public static synchronized void sendEmail()
{
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----sendEmail");
}
public synchronized void sendSMS()
{
System.out.println("-----sendSMS");
}
public void hello()
{
System.out.println("-------hello");
}
}
问题:
-
标准访问有ab两个线程,请问先打印邮件还是短信
-
sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
-
添加一个普通的hello方法,请问先打印邮件还是hello
-
有两部手机,请问先打印邮件还是短信
-
有两个静态同步方法,有1部手机,请问先打印邮件还是短信
-
有两个静态同步方法,有2部手机,请问先打印邮件还是短信
-
有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
-
有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。
当然一个对象中有其他的普通方法,如果同时调用,两个没有争抢关系,正常执行!
锁的是对象的时候,不同的对象之间的synchronized方法不存在竞争关系,各执行各的。
都换成静态同步方法后,情况又变化,对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁------->实例对象本身,对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板。
#对象锁
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
•
#类锁
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
再高并发的时候,同步调用应该去考虑锁的性能消耗,能用无锁数据结构,就不要用锁;能锁区块的就不要锁整个方法体;能锁对象的就不要锁类。
1.3、公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人再队尾排队等待ReentrantLock lock = new ReentrantLock(true);
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,再高并发的环境下,有可能造成优先级翻转或者饥饿的状态(饥饿就是一个线程一直得不到锁)ReentrantLock lock = new ReentrantLock();或者ReentrantLock lock = new ReentrantLock(flase);表示非公平。
import java.util.concurrent.locks.ReentrantLock;
/**
* @BelongsProject: JUC_Learn
* @BelongsPackage: com.songzhishu.juc.lock
* @Author: 斗痘侠
* @CreateTime: 2024-03-27 10:44
* @Description: TODO
* @Version: 1.0
*/
public class SaleTicketDemo {
public static void main(String[] args)//一切程序的入口
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <55; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 0; i <55; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 0; i <55; i++) ticket.sale(); },"c").start();
}
}
class Ticket //资源类,模拟3个售票员卖完50张票
{
private int number = 50;
//Object lockObject = new Object();
ReentrantLock lock= new ReentrantLock(true);//可重入锁 递归锁 synchronized是非公平锁 ReentrantLock可以设置公平锁
public void sale()
{
lock.lock();
try {
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
}
} finally {
lock.unlock();
}
}
}
恢复挂起的线程到真正锁的获取还是有时间差的,再我们看来这个时间微乎其微,但是从cpu的角度看,这个时间差采存在还是很明显的,所以非公平锁能够充分的利用cpu的时间片,尽量减少cpu的空闲时间。使用多线程需要考虑的是线程切换的开销,采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程再此刻2再次获取同步状态的概率就变的非常的大,再一定程度上减少了线程的开销。
1.4、可重入锁
可重入锁又叫做递归锁,什么意思呐,就是一个线程再外层方法获取锁的时候,再次进入该线程的内层方法会自动获取锁(前提锁对象是同一个),不会因为之前已经获取锁但是没释放而阻塞。可重入锁在一定程度上可以避免死锁。
每一个锁对象拥有一个锁计数器和一个指向持有该锁的指针,当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,java虚拟机会将该锁对象的持有线程修改为当前线程,并且计数器加1,在目标锁对象计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直到持有的线程释放该锁
隐式(synchronized)默认是可重入锁,在一个Synchronized修饰的方法或者代码的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以获取到锁对象的。
private static void reEntryM1()
{
final Object object = new Object();
new Thread(() -> {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----外层调用");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----中层调用");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t ----内层调用");
}
}
}
},"t1").start();
}
1.5、死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
诱发死锁的原因:
-
互斥条件
-
占用且等待
-
不可抢夺(或不可抢占)
-
循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁: 死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
-
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
-
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题,对共享的资源要么全申请完毕要么全不申请。
-
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
-
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
查看当前是否有死锁两种方式
-
方式一:终端下jsp -l 配合jstack 进程编号
-
方式二:图像化工具 win+r jconsole
2、中断机制和LockSupport
2.1、线程的中断机制
首先一个线程不应该由其他的线程来强制中断或者停止,而是应该由线程自己自行停止,自己决定自己的命运,所以Thread.stop、Thread.suspend、Thread.resume都已经遗弃。其次,在java中没有办法立即停止一条线程,然而停止线程区却显得尤为重要,如取消一个耗时的操做,因此,java提供了一种用于停止线程的协商机制---中断,以就是中断标识协商机制。
中断只是一种协商机制,java没有给中断添加任何的语法,中断的过程完全需要程序员自己实现,若要中断一个线程,你需要手动的调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设置为true,接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求中断当前线程,但是具体的中断还是由线程本身执行!
每一个线程对象中都有一个中断的标识位,用于表示线程是否被中断,该标识位为true表示中断,为false表示为中断;通过调用线程对象的interrupt方法将该线程的标识位设置位true,可以在别的线程中调用,也可以在自己的线程中调用。
-
public void interrupt() 实例方法仅仅是设置线程的中断状态为True,发起一个协商但是不会立即停止线程。
-
public static boolean interrupted() 静态方法 判断线程是否被中断,并且清除当前的中断状态
-
返回当前线程的中断状态,测试当前线程是否已被中断
-
将当前线程的中断状态清零并重新设置为false,清除线程的中断状态。
-
-
public boolean isInterrupted() 实例方法 判断当前线程是否被中断
使用volatile中断线程
public class interruptDemo_volatile {
static volatile boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (true) {
if (isStop){
System.out.println(Thread.currentThread().getName()+"isStop为true,终止程序");
break;
}
System.out.println("正常执行");
}
},"t1").start();
Thread.sleep(20);
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"修改状态");
isStop=true;
},"t2").start();
}
}
使用AtomicBoolean中断线程
private static void m2() throws InterruptedException {
new Thread(() -> {
while (true) {
if (atomicBoolean.get()){
System.out.println(Thread.currentThread().getName()+"isStop为true,终止程序");
break;
}
System.out.println("正常执行");
}
},"t1").start();
Thread.sleep(20);
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"修改状态");
atomicBoolean.set(true);
},"t2").start();
}
使用线程中的API来实现
private static void m3() {
Thread thread = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "被中断了");
break;
}
System.out.println("正常执行");
}
}, "t1");
thread.start();
try {
TimeUnit.MICROSECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
/*new Thread(()->{
thread.interrupt();
},"t2").start();*/
//中断线程
thread.interrupt();
}
具体来说,对一个线程,调用interrupt方法,如果线程处于正常状态,那么会将该线程的中断标识设置为true,仅此而已,被设置中断标识的线程将继续正常运行,不受影响。所以说interrupt()不能真正的中断线程,需要被调用的线程自己进行配合才可以。如果线程处于被阻塞的状态(sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个interruptException异常。注意:中断标识位也会被清空置为false!
欧克,来说说interrupted方法吧!
public class interruptDemo1 {
public static void main(String[] args) {
//测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个false。
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println("----1");
Thread.currentThread().interrupt();// 中断标志位设置为true
System.out.println("----2");
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
//LockSupport.park();
//Thread.interrupted();//静态方法
//Thread.currentThread().isInterrupted();//实例方法
}
}
2.2、线程唤醒机制
唤醒线程的方式:
-
使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程。
-
使用JUC包中的Condition的await()方法让线程等待,使用Signal()方法唤醒线程
-
LockSupport类可以阻塞当前线程,以及唤醒指定被阻塞的线程。
方法一:
public class Sync {
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "\t" + "come in");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
}
}, "t1").start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
synchronized (o) {
o.notify();
System.out.println(Thread.currentThread().getName() + "\t" + "通知");
}
}, "t2").start();
}
}
注意:执行wait和notify的方法时要保证在同步代码块里,否者会报异常;将notify和wait的执行顺序颠倒会导致线程无法被唤醒。
方法二:
public class condi {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "come in");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
} finally {
reentrantLock.unlock();
}
}, "t1").start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
reentrantLock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t" + "通知");
} finally {
reentrantLock.unlock();
}
}, "t2").start();
}
}
注意:Condtion中的线程等待和唤醒方法,都需要获取锁才可以,而且要先await后signal,不能反了!
方法三:通过park和unpark方法来实现阻塞和唤醒线程的操作
public class support {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "come in");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
}, "t1");
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "通知");
LockSupport.unpark(thread);
}, "t2").start();
}
}
既然是压轴的,没有锁块的要求,是个螺丝钉安在哪里用哪里,在之前,先唤醒的后阻塞会出异常,但是这个不会,注意的是还是要成对的出现!
总结:
LockSupport是一个用来创建锁和其他同步类的基本线程阻塞原语,LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应唤醒方法,LockSupprot和每一个使用它的线程都有一个许可(permit)关联。每一个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会累计凭证!改变执行顺序也可以的原因是因为凭证!
3、JMM
因为cpu和内存的速度不一致,CPU的运行不是直接操作内存而是先把内存中的数据读取到高速缓存中,而内存的读写的时候就不会出现的不一致问题。
JVM中定义一种java memory model 简称JMM来屏蔽到各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都可以达到一致的内存访问效果。
JMM内存模型本身是一种抽象的概念并不是一种真实存在的,它只是描述的一种约定或者规范,通过这组棍法定义了程序中(尤其多线程)各各变量的读写访问权限,并决定一个线程对共享变量的何时写入,以及如何变成对另外线程可见,关键是在多线程的原子性、可见性和有序性展开的。
3.1、特性
3.1.1、可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道变更,JMM规定所有的变量存储在主内存中。先copy一个共享变量到本地,修改完毕之后再写回主内存!
系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
3.1.2、原子性
是指一个操作是不可以打断的,即多线程环境下,操作不能被其他线程干扰。
3.1.3、有序性
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
优缺点 JM能根据处理器特性(CPU多级缓存系统、多核处理器等〕适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
但是,指令重排可以保证串行语义一致,但没有义务保证 多线程间的语义也一致(即可能产生"脏读"),简单说就是,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
3.1.4、happens—before
如果一个操作happens--before另外一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。如果两个操作之间存在happens-before关系,并不意味着一定要按照happends-before原则制定的顺序来执行,如果重排序之后的执行结果与按照happends-before关系来执行的结果一致,那么这种重排序并不非法。可见---顺序
细化的说:
-
次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生与后面的操作,简单的说,就是前一个操作的结果可以被后续的操作获取。
-
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
-
volatile变量控制:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间上的先后。
-
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
-
线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作。