2.并发编程---常用的关键字

本文深入探讨了Java中的对象锁、类锁的概念及其区别,通过示例展示了它们在多线程环境中的行为。同时,解释了Volatile关键字的作用及线程不安全性,并通过实例展示了ThreadLocal的线程数据隔离特性。最后,讨论了wait、notifyAll、notify等同步原语在控制线程协作中的应用。通过对这些概念的深入理解,有助于提升Java并发编程能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常用的synchronized锁

对象锁和类锁的区别
对象锁:一般直接用synchronized修饰某个方法即可
类锁:一般是修饰static的方法,相当于是修饰整个类,所以称类锁
总结:如果是同一个对象调用对象锁的方法时候,他们会按照顺序去执行方法,如果是不同的对象调用对象锁的方法,他们会异步去执行方法。
如果是类锁,不管你是否是同一个对象,只要是方法属于类锁,他们都会按照顺序去执行方法。

代码演示类锁和对象锁

以后通用的线程睡眠工具类

public class SleepTools {
    //按秒休眠
    public static final void sencod(int s){
        try {
            TimeUnit.SECONDS.sleep(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //毫秒休眠
    public static final void ms (int ms){
        try {
            TimeUnit.MICROSECONDS.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建一个SycnzAndIns类。有以下两个方法

public class  SycnzAndInst {
    //该方法是对象锁
    public  synchronized void syncObj(){
        System.out.println(Thread.currentThread().getName()+"-----执行中---");
        SleepTools.sencod(3);
    }
    //该方法是SycnzAndInst类锁
    public synchronized static void syncClass(){
        System.out.println(Thread.currentThread().getName()+"-----执行中---");
        SleepTools.sencod(3);
    }
}

在创建两个线程类,并且用构造方法注入SycnzAndIns对象

class  InstancSyncThread1 extends Thread{
    //使用构造方法注入SycnzAndInst对象
    private SycnzAndInst sycnzAndInst;
    public InstancSyncThread1(SycnzAndInst sycnzAndInst){
        this.sycnzAndInst = sycnzAndInst;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---开始---");
        sycnzAndInst.syncObj();
        SleepTools.sencod(1);
        System.out.println(Thread.currentThread().getName()+"---结束---");
    }
}

class InstancSyncThread2 extends Thread{

    //使用构造方法注入SycnzAndInst对象
    private SycnzAndInst sycnzAndInst;
    public InstancSyncThread2(SycnzAndInst sycnzAndInst){
        this.sycnzAndInst = sycnzAndInst;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---开始---");
        sycnzAndInst.syncObj();
        SleepTools.sencod(1);
        System.out.println(Thread.currentThread().getName()+"---结束---");
    }
}

在SycnzAndInst类中写上main函数。先测试对象锁

 public static void main(String[] args) {
        //注意这里,创建同一个对象
        SycnzAndInst sycnzAndInst = new SycnzAndInst();
        InstancSyncThread1 syncThread1 = new InstancSyncThread1(sycnzAndInst);
        syncThread1.setName("线程1:");
        InstancSyncThread2 syncThread2 = new InstancSyncThread2(sycnzAndInst);
        syncThread2.setName("线程2:");
        //开启启动两个线程
        syncThread1.start();
        syncThread2.start();
    }
线程1---开始---
线程1-----执行中---
线程2---开始---
线程2-----执行中---
线程1---结束---
线程2---结束---

1:我们可以看到锁对象的时候,这时候,两个线程,如果线程1正在调用syncObj()方法时候,那么线程2就必须等待线程1把syncObj()方法执行完后,才可以使用syncObj()方法。也就是说synchronized 锁对象时候,同一个对象调用的方法必须等待另一个调用结束才可以进行调用。
2:如果是两个不同的对象,调用加synchronized修饰的方法时候,采取的是异步执行

 public static void main(String[] args) {
        //注意这里,通过new SycnzAndInst()创建两个不同的的对象
        InstancSyncThread1 syncThread1 = new InstancSyncThread1(new SycnzAndInst());
        syncThread1.setName("线程1:");
        InstancSyncThread2 syncThread2 = new InstancSyncThread2(new SycnzAndInst());
        syncThread2.setName("线程2:");
        //开启启动两个线程
        syncThread1.start();
        syncThread2.start();
    }
线程1---开始---
线程2---开始---
线程2-----执行中---
线程1-----执行中---
线程2---结束---
线程1---结束---

从结果可以方法对象锁只对当前对象起效果,如果是不同对象调用加锁的方法,他们是采用异步执行的。
接下来看下类锁
1.不同对象调用 synchronized static 修饰的同一个方法

 public static void main(String[] args) {
        //注意这里,通过new SycnzAndInst()创建两个不同的的对象
        InstancSyncThread1 syncThread1 = new InstancSyncThread1(new SycnzAndInst());
        syncThread1.setName("线程1:");
        InstancSyncThread2 syncThread2 = new InstancSyncThread2(new SycnzAndInst());
        syncThread2.setName("线程2:");
        //开启启动两个线程
        syncThread1.start();
        syncThread2.start();
    }
线程1---开始---
线程1-----执行中---
线程2---开始---
线程2-----执行中---
线程1---结束---
线程2---结束---

2.同一个对象调用synchronized static修饰的同一个方法

public static void main(String[] args) {
        SycnzAndInst sycnzAndInst = new SycnzAndInst();
        InstancSyncThread1 syncThread1 = new InstancSyncThread1(sycnzAndInst);
        syncThread1.setName("线程1:");
        InstancSyncThread2 syncThread2 = new InstancSyncThread2(sycnzAndInst);
        syncThread2.setName("线程2:");
        //开启启动两个线程
        syncThread1.start();
        syncThread2.start();
    }
线程1---开始---
线程1-----执行中---
线程2---开始---
线程2-----执行中---
线程1---结束---
线程2---结束---

我们发现不管是否是同一个对象调用类锁的方法时候,他们总是按照顺序执行加锁的方法的,也就是说只要是类锁的方法,不管对象是否是同一个,都必须等待上一个线程把方法执行完毕后,才可以轮到下一个线程执行方法

Volatile关键字

作用: 保证变量在内存的可见性。但是无法保证原子操作,属于线程不安全
推荐使用场景:一个线程写,多个线程读
代码演示线程不安全的情况。当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

class RunableThread implements Runnable{
    private  volatile  int nums = 0;
    @Override
    public void run() {
        //操作系统的运算符不是原子操做,一个+操作,虚拟机可能要执行好几条指令才能完成+操作
        nums= nums+10;
        SleepTools.ms(100);
        nums= nums+10;
        System.out.println(Thread.currentThread().getName()+"--nums="+nums);

    }
}
/**
 * volatile保证变量在内存的可见性,但是不保证原子性
 *
 * **/
public class VolatileUnsafe {

    public static void main(String[] args) {
        //创建线程的一个实列
        RunableThread runableThread = new RunableThread();
        //下面四个线程共享nums
        Thread A = new Thread(runableThread, "A:");
        Thread B = new Thread(runableThread, "B:");
        Thread C = new Thread(runableThread, "C:");
        Thread D = new Thread(runableThread, "D:");
        A.start();
        B.start();
        C.start();
        D.start();

    }
}
B:--nums=60
C:--nums=50
D:--nums=70
A:--nums=50

理论上输出的数据应该是20-40-60-80的,但是 结果很奇葩。这也就间接的说明。Volatile是线程不安全的。

ThreadLocal

可以理解成Map<Thread,Object>
作用: 主要作用是数据隔离,ThreadLocal修饰的变量,相当于一个副本每个线程都可以单独共享这个副本。也就是说被ThreaLocal修饰的变变量,只属于当前线程的。其他线程无法进行修改。每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。
举个例子。经ThreadLocal修饰的是变量nums=0。这时候与ABC三个线程去修改这个值)(nums+=1)。最终结果都是输出1,并不会出现1,2,3或者其他情况。
缺点:由于每个线程都共享一个副本,ThreadLocal修饰的变量不应该是占用内存大的变量比如对象、JSON等等。简称用空间换取线程的安全性

代码实现

public class ThreadLocals {
    //定义一个ThreadLocal修饰的变量。赋值默认值为1
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };

    //创建三个线程
    public void stardTreads(ThreadLocals threadLocals) {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = creatRunable("线程["+i+"]",i);
        }
        //启动线程
        for (Thread thread : threads) {
            thread.start();
        }
    }

    //线程
    public Thread creatRunable(String threadName,Integer addNums) {
        Thread thread = new Thread(() -> {
            SleepTools.ms(100);
            System.out.println(Thread.currentThread().getName() + "statr....");
            threadLocal.set(threadLocal.get()+addNums);
            //打印操作后的值
            System.out.println(Thread.currentThread().getName() + "end...." + threadLocal.get());
            SleepTools.ms(100);
        },threadName);
        return thread;
    }

    public static void main(String[] args) {
        ThreadLocals threadLocals = new ThreadLocals();
        threadLocals.stardTreads(threadLocals);
        //等待所有线程结束后,打印下threadLocal里面的值
        while (Thread.activeCount()>1){
            System.out.println("threadLocal = "+threadLocals.threadLocal.get());
        }
    }
}
线程[1]statr....
线程[1]end....2
线程[0]statr....
线程[0]end....1
线程[2]statr....
线程[2]end....3
threadLocal = 1

我们发现输出的结果,线程0:0+1=1;线程1:1+1=2;线程2:2+1=3.。可以看见每个线程取ThreadLocal里面的值,都是相互独立的。而所有线程结束后,原来的默认值并没有改变。并且set后的值,并不影响一开始的Integer initialValue()方法设置的默认值,这说明数据已经被隔离了。

Wait和NotifyAll、Notify使用

如果对象调用了wait就会把当前锁释放,然后该线程进入wait等待队列中。

如果对象调用了notify方法就会唤醒wait等待队列中等待的线程。

如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

场景:现在有ABC三个线程A负责输出A,B输出B,C输出C ,要求打印出五次ABC,ABC,ABC,ABC,ABC

/**
 * 演示wait和notify\notifyAll
     * 模糊场景:现在有ABC三个线程A负责输出A,B输出B,C输出C ,要求打印出五次ABC,ABC,ABC,ABC,ABC
 **/
public class WnThread {
    Integer flage = 1;

    public synchronized void pritlnA() {
        //如果flage不等于1,打印A的线程进入等待
        while (flage != 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //输出A
        System.out.print("A");
        //接下来输出B
        flage = 2;
        //唤醒所有在等待的线程
        notifyAll();
    }

    public synchronized void pritlnB() {
        //如果flage不等于1,打印A的线程进入等待
        while (flage != 2) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //输出A
        System.out.print("B");
        //接下来输出B
        flage = 3;
        notifyAll();
    }

    public synchronized void pritlnC() {
        //如果flage不等于1,打印A的线程进入等待
        while (flage != 3) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //输出A
        System.out.print("C");
        //接下来输出B
        flage = 1;
        notifyAll();
    }

    public static void main(String[] args) {
        WnThread wnThread = new WnThread();
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                wnThread.pritlnA();
            }
        });
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                wnThread.pritlnB();
            }
        });
        Thread threadC = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                wnThread.pritlnC();
            }
        });
        //启动三个线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

Join

作用: 获取执行权(插队,会让当前线程进入阻塞状态)。当线程A需要调用B线程的join方法时候,就必须等待着B的线程执行完成后,A线程才会继续执行。
举个例子:在食堂打饭的时候需要进行排队,这时候你看见有一个美女过来准备排队,你很高兴的把她叫到你前面进行排队了,这时候,你必须等待这个美女打完饭你才可以打饭。

public class JoinThread {

    /**
     * thread,线程A,线程B将插队,A线程直至B线程执行完,才会继续执行
     * **/
    public Thread createThreadA(Thread threadB){
        return new Thread(()->{
            try {
                System.out.println("A线程开始执行。。。。。");
                threadB.join();
                System.out.println("A线程执行结束。。。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A");
    }
    /**
     * thread,线程B
     * **/
    public Thread createThreadB( ){
        return new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"B线程在执行将休息三秒....");
            SleepTools.sencod(3);
            System.out.println("B线程休息结束.....");
        });
    }


    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"--执行中--");
        JoinThread joinThread = new JoinThread();
        Thread threadB = joinThread.createThreadB();
        Thread threadA = joinThread.createThreadA(threadB);
        threadA.start();
        threadB.start();
        SleepTools.sencod(5);
        System.out.println(Thread.currentThread().getName()+"---执行结束--");

    }
}

看下面的输出结果,很明显看到A线程里面调用B的join方法时候,A必须等待B线程所有的代码执行完成后,才可以继续往下执行。即使B线程先执行,那A线程调用B的join,也必须等待B线程执行完

main--执行中--
A线程开始执行。。。。。
B线程在执行将休息三秒....
B线程休息结束.....
A线程执行结束。。。。。
main---执行结束--

yield、sleep、wait、notifyall

在这里插入图片描述

yield和sleep:都可以让线程进入睡眠状态的,但是他两个不会释放线程的持有锁。不同点在于,yield睡眠的线程时间到了之后,会进入就绪状态,线程可能再次继续执行。但是Sleep睡眠时间到了之后,会进入阻塞状态的,进行重新排队,不会再次执行。
wait:在使用wait进行线程等待之前,该线程一定要持有锁,调用wait方法,该锁会被释放掉,线程进入阻塞状态。
notify与notifyAll:作用都是唤醒被wait方法修饰的线程,但是不同的是,notify会随机唤醒一个等待的线程,但是notifyAll会唤醒所有等待的线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值