LOADING

加载过慢请开启缓存 浏览器默认开启

2023/11/14

每日0题+Java学习:多线程

多线程的锁机制讲的很清楚很好。

例如,对于语句:

n = n + 1;

看上去是一行语句,实际上对应了3条指令:

ILOAD
IADD
ISTORE

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

┌───────┐    ┌───────┐
│Thread1│    │Thread2│
└───┬───┘    └───┬───┘
    │            │
    │ILOAD (100) │
    │            │ILOAD (100)
    │            │IADD
    │            │ISTORE (101)
    │IADD        │
    │ISTORE (101)│
    ▼            ▼

如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102

这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

┌───────┐     ┌───────┐
│Thread1│     │Thread2│
└───┬───┘     └───┬───┘
    │             │
    │-- lock --   │
    │ILOAD (100)  │
    │IADD         │
    │ISTORE (101) │
    │-- unlock -- │
    │             │-- lock --
    │             │ILOAD (101)
    │             │IADD
    │             │ISTORE (102)
    │             │-- unlock --
    ▼             ▼

通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
    n = n + 1;
}
package java_thread;


//框选模型:解释java类间继承以及相互调用
public class _create {
    public static void main(String[] args) throws InterruptedException{
        System.out.println("JVM 主线程main启动");
        infinite_thread inf=new infinite_thread();
        inf.setDaemon(true);  //设置成守护线程,守护线程是默认无限执行的,main线程默认会等待所有线程执行结束自己才会结束,除了守护线程(相当于允许守护线程在main线程之后结束
        //在守护线程中,编写代码要注意:守护线程不能持有任何!!需要关闭的资源!!,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
        inf.start();
        Thread.sleep(100);  //main方法就相当于一个线程
        /*
        main线程肯定是先打印main start,再打印main end;
        t线程肯定是先打印thread run,再打印thread end。
        但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。
        !!!因为从t线程开始运行以后,两个线程就开始同时运行了,并且由!!操作系统!!调度,程序本身无法确定线程的调度顺序。!!!
         */
        Mythread t1=new Mythread();  //实现自己的线程:自己写个类继承一个Thread并覆写run方法,线程启动时会自动执行run方法,run方法执行结束线程也就结束了
        t1.start();
        t1.running=false; //通过将t1.running共享变量设为false可以将该线程停止
        //t1.run(); //直接调用run方法相当于把run方法写到_create类里然后直接调用,暂停的也是main! 这行代码没有任何线程创建
        Thread t2=new Thread(()->{
           System.out.println("started a new thread2");  //使用lambda语法来快速覆写run方法
        });
        //设置线程优先级,优先级越高操作系统对该线程的调度就越频繁(因为任务数>>cpu核心数,执行任何线程任务都是轮流执行一定时间),执行速度“可能”就越快,但是不一定保准
        t2.setPriority(10);
        t1.join(); //当前线程(main)等待t1线程执行结束
        System.out.println("t1线程执行结束");
        t2.start();
        t2.interrupt(); //中断进程,如果该进程在等待,会抛出异常,其他状态似乎会退出


    }
    /*
New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕。
     */
}

class Mythread extends Thread{
    //线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

    //volatile关键字的目的是告诉虚拟机:
    //每次访问变量时,总是获取主内存的最新值;
    //每次修改变量后,立刻回写到主内存。
    public volatile boolean running=true;
    @Override
    public void run(){
        System.out.println("mythread begin");
        try {
            Thread.sleep(1000);
            if(this.running==false) System.out.println("接收到running变量改变,但这里就不停止了");
        }catch (InterruptedException e){};
        System.out.println("mythread end");
    }
}

class infinite_thread extends Thread{
    @Override
    public void run(){
        int n=0;
        while (true){ //无尽循环的守护线程,允许在main线程之后结束
            n++;
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){  //如果在等待过程中被中断

            }
            System.out.println("%d秒经过".formatted(n));
        }
    }
}
package java_thread;

public class synchronization {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock=new Object();
    public static int count = 0;
}

class slock{
    public static final Object lock=new Object();
}
class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized (slock.lock) {   //获得锁后,才能执行后续的操作
                //锁只是相当于一块控制内存(只不过必须是Object类型实例),如果该内存被其他线程占用,就无法获得这块控制内存,同步体的代码块也就无法执行,
                // 只不过一般会在操作变量所在的类内设置一个锁内存

                //JVM规范定义了几种默认原子操作:(即这些代码操作一次只能被一个类执行)
                //基本类型(long和double除外)赋值,例如:int n = m;
                //引用类型赋值,例如:List<String> list = anotherList。
                //!注:这并不表明这些赋值操作就不用再被放进同步框内,因为可能有单线程连续操作变量的情况,虽然对x赋值操作是原子操作,但是其他线程可以同时操作y赋值!

                Counter.count += 1;}
        }
    }
    public synchronized static void test(int n) {}  //synchronized关键字相当于该方法! 默认以当前类的实例为锁内存,静态方法则默认以当前静态类的.class(也就是静态内存)为锁内存
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized (slock.lock) {
                Counter.count -= 1;
            }
        }
    }
}