什么是线程(上篇)

什么是线程(上篇)

线程概念

引入进程的目的,就是为了能够"并发编程",虽然多进程已经能够解决并发的问题了,但是我们认为,还不够理想.因为进程是系统资源分配的基本单位,创建进程,就需要分配资源,销毁进程,就需要释放资源,如果频繁创建销毁,这样的开销就比较大了.于是程序猿就发明了一个"线程"(Thread)概念.线程在有些系统上也叫做"轻量级进程"

什么是轻量级:

创建线程比创建进程更高效. 销毁线程比销毁进程更高效. 调度线程比调度进程更高效.

其实创建线程,并没有去申请资源,销毁线程,也不需要释放资源.而是让线程产生在进程内部,共用之前的资源

进程和线程之间是什么关系

包含关系.一个进程可以包含一个线程或者多个线程.系统先把进程创建出来之后,这个时候,相当于资源都分配好了.然后再在这个进程里面,创建线程这样的线程就和之前的进程共用一样的资源了

系统内核角度理解进程和线程

在这里插入图片描述

进程和线程之间的区别和联系: [经典面试题]

1.进程是包含线程的. 一个进程里可以有一个线程,也可以有多个线程.
2.每个进程都有独立的内存空间(虚拟地址空间).同一个进程的多个线程之间共用这个虚拟地址空间.
3.进程是操作系统资源分配的基本单位.线程是操作系统调度执行的基本单位.
实际上系统是以线程为单位进行调度(以PCB为单位进行调度)

在这里插入图片描述

线程和代码是什么关系

一个线程就是代码中的一个 “执行流”. 每个线程之间都可以按照顺序来执行自己的代码. 多个线程之间 “同时” 执行着多份代码.

创建多线程程序的方法

1.通过继承Thread,重写run .
2.实现Runnable接口,重写run.
3.继承Thread,重写run,使用匿名内部类的方式.
4.实现Runnable,重写run,使用匿名内部类
5.使用lambda表达式来表示要执行的任务

/**
 * 1.通过继承Thread,重写run .
 */
//Thread是Java标准库中描述一个关于线程的类
//常用的方法就是自己定义一个类继承Thread.然后重写Thread中的run方法,
// run方法就是表示线程要执行的具体代码
class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        System.out.println("hello thread!");
    }
}
public class ThreadDemo1 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new MyThread();
        //start方法就是会在操作系统中真的创建一个线程出来(内核搞个PCB,加入到双线链表中)
        //这个新的线程就会执行run中所描述的代码
        t.start();
        t.run();//虽然结果一样,但是内部的意义天差地别
    }

}
/**
 * 2.实现Runnable接口,重写run.
 */
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        while (true){
    
    
            System.out.println("hello thread!");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo3 {
    
    
    public static void main(String[] args) {
    
    
        //把MyRunnable的实例作为Thread的参数
        //本质上和继承Thread重写run方法效果一样
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}
/**
 * 3.继承Thread,重写run,使用匿名内部类的方式.
 * 匿名内部类实现多线程
 */
public class ThreadDemo4 {
    
    
    public static void main(String[] args) {
    
    
        //使用匿名内部类
        //此处我们new的实例,其实是new了这个新的子类的实例然后继承了Thread
        Thread t = new Thread(){
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    System.out.println("hello thread!");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }
}
/**
 * 4.实现Runnable,重写run,使用匿名内部类~
 */
public class ThreadDemo5 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while (true){
    
    
                    System.out.println("hello thread!");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }
}
/**
 *5.使用lambda表达式来表示要执行的任务~
 */
public class ThreadDemo6 {
    
    
    //本质上就是一个匿名函数,通过函数式接口的方式来实现的.
    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            while(true){
    
    
                System.out.println("hello thread!");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

以上这些创建线程的方式,本质都相同.都是借助Thread类,在内核中创建新的PCB,加入到内核的双向链表中.只不过区别是,指定线程要执行的任务的方式不一样.
此处的区别,其实都只是单纯的java语法层面的区别~

t.start()t.run()区别

在这里插入图片描述

代码如下:

/**
 * `t.start()`和`t.run()`区别
 */
class MyThread2 extends Thread{
    
    
    @Override
    public void run() {
    
    
        while (true){
    
    
            System.out.println("hello thread!");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo2 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new MyThread2();
        //t.start();
        t.run();
        while (true){
    
    
            System.out.println("hello main");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

使用 jconsole 命令观察线程

在这里插入图片描述

多线程的优势

增加运行速度

/**
 * 多线程的优势-增加运行速度
 */
public class ThreadDemo7 {
    
    
    public static final long count = 10_0000_0000L;
    //串行的来计算针对a和b进行自增
    public static void serial(){
    
    
        //获取当前系统毫秒级时间戳
        long beg = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
    
    
            a++;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
    
    
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("time " + (end - beg));
    }

    //并发的来计算针对a和b进行自增
    public static void concurrency(){
    
    
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                int a = 0;
                for (long i = 0; i < count; i++) {
    
    
                    a++;
                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                int b = 0;
                for (long i = 0; i < count; i++) {
    
    
                    b++;
                }
            }
        };
        t2.start();

        //因为concurrency(),t1,t2这些线程都是并发执行的
        //我们需要保证t1 和 t2 都要执行完之后来结束记时
        try {
    
    
            //join()就是等待对应的线程结束完后的一种阻塞等待方法
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("time " + (end - beg));
    }
    public static void main(String[] args) {
    
    
        System.out.print("串行执行时间: ");
        serial();
        System.out.print("并发执行时间: ");
        concurrency();
    }
}

代码结果:
串行执行时间: time 679
并发执行时间: time 394

速度确实是提高了!!但是时间却不正好是缩短了一倍?
因为线程调度,自身也是有开销的 其中串行一个线程执行了20亿次循环.中间可能会调度线程若干次. (会花一定的时间)
两个线程各自执行10亿次循环.中间也可能会调度线程若干次… (也会花一定的时间).但是这两组调度的时间具体是多少是完全不确定的,所以总共花费的时间不是刚好缩短了一倍.

线程共享资源具体指的是什么?

所说的"共享的资源"最主要指的是两方面:

1.内存.线程1和线程2都可以共享同一份内存(同一个变量)

变量就是内存, 两个线程能共同访问同一个变量,那么这俩线程在使用同一个内存(比如上述代码t1线程和t2线程都是共同访问同一个count的具体值)
而对于多个进程来说,进程1就不能访问进程2的变量.

2.文件.线程1打开的文件,线程2也能去使用

Thread 类的具体用法

Thread 的常见构造方法

在这里插入图片描述

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

举例使用 Runnable 对象创建线程对象,并命名

/**
 * 使用 Runnable 对象创建线程对象,并命名
 */
public class ThreadDemo8 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while (true){
    
    
                    System.out.println("hello thread!");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        },"MyThread");
        t.start();
    }
}

在这里插入图片描述

Thread 的几个常见属性

在这里插入图片描述

1)ID 是线程的唯一标识,不同线程不会重复
2)名称是各种调试工具用到
3)状态表示线程当前所处的一个情况
4)优先级高的线程理论上来说更容易被调度到
5)关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。

创建的一个线程,**默认不是后台线程.**此时,如果main方法结束了,线程还没结束, JVM进程不会结束.如果当前线程是后台线程.此时如果main方法结束了,线程还没结束, JVM进程就会直接结束.同时也就把这个没结束的线程也结束了
6)是否存活,即简单的理解,为 run 方法是否运行结束了

理论上来讲当系统内核的PCB这里的代码都执行完了,随之这里的PCB (内核中的线程)就销毁了.而用户代码中的new的这个对象,要靠GC(JVM垃圾回收机制)来销毁.所以Thread t的生命周期和内核的PCB不一样(比PCB更长)

7)线程的中断问题

/**
 * Thread 的几个常见属性
 */
public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    //Thread.currentThread()静态方法来打印当前线程的名字
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"MyThread");
        t.start();

        //在这里打印这个线程的属性
        System.out.println("id: " + t.getId());
        System.out.println("name: " + t.getName());
        System.out.println("state: " + t.getState());
        System.out.println("priority: " + t.getPriority());
        System.out.println("isDamon: " + t.isDaemon());
        System.out.println("isInterrupted: " + t.isInterrupted());
        System.out.println("isAlive: " + t.isAlive());
    }
}

在这里插入图片描述

中断一个线程

1.简单粗暴的方法,就是使用一个boolean变量来作为循环结束标记

/**
 * 如何控制线程
 * 1.简单粗暴的方法,就是使用一个boolean变量来作为循环结束标记
 */
public class ThreadDemo10 {
    public static boolean flag = true;
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(flag){
                    System.out.println("线程运行中....");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程结束!");
            }
        };
        t.start();

        //主循环等待三秒后,把flag改为false
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
    }
}

2.使用标准库里内置的标记.
获取线程内置的标记位:线程的isInterrupted()判定当前线程是不是应该要结束循环.
修改线程内置的标记位: Thread.interrupt()来修改这个标记位.

这里的interrupt方法可能有两种行为:
1.如果当前线程正在运行中,此时就会修改Thread.currentThread().isInterrupted() 标记位为true
2.如果当前线程正在sleep / wait /等待锁…此时会触发InterruptedException(解决办法:加上 break 退出即可)

/**
 * 2.使用标准库里内置的标记.
 * 获取线程内置的标记位:线程的`isInterrupted()`判定当前线程是不是应该要结束循环.
 * 修改线程内置的标记位: `Thread.interrupt()`来修改这个标记位.
 */
public class ThreadDemo11 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(){
    
    
            @Override
            public void run() {
    
    
                //Thread.currentThread()获取当前线程实例
                //isInterrupted默认情况下是false
                //while (!Thread.interrupted()){
    
    
                while(!Thread.currentThread().isInterrupted()){
    
    
                    System.out.println("线程运行中...");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                        //如果当前线程正在sleep / wait /等待锁...此时会触发`InterruptedException`
                        // (解决办法:**加上 `break` 退出即可**)
                        break;
                    }
                }
                System.out.println("线程终止!");
            }
        };
        t.start();

        //再主线程中,通过t.interrupted()方法来设置这个标记位
        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //t.interrupt()把Thread.currentThread().isInterrupted()设为true
        t.interrupt();
    }
}

interrupt()isInterrupted()区别

在这里插入图片描述

isInterrupted()这个是Thread实例方法 interrupted()这个是Thread类方法(static )
这两者有个区别:使用这个静态的方法,会自动清除标记位.
例如,调用interrupt()方法,把标记位设为true, 就应该结束循环.
当调用静态的interrupted()来判定标记位的时候就会返回true,同时就会把标记位再改回成false.下次再调用interrupted)就返回false.(好比开关按下去会自动弹起来)
如果是调用非静态的isInterrupted()来判定标记位也会返回true.同时不会对标记位进行修改.后面再调用isInterrupted()的时候就仍然是返回true(好比开关按下去不会自动弹起来)

public class ThreadDemo12 {
    
    
    private static class MyRunnable implements Runnable {
    
    
        @Override
        public void run() {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.println(Thread.interrupted());
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
    }
}
运行结果:
true
false
false
false
false
false
false
false
false
false
/**
 * interrupt()与isInterrupted()区别
 */
public class ThreadDemo12 {
    
    
    private static class MyRunnable implements Runnable {
    
    
        @Override
        public void run() {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.println(Thread.currentThread().isInterrupted());
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
    }
}
运行结果:
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true

等待一个线程join()

线程等待.
线程和线程之间,调度顺序是完全不确定(取决于操作系统调度器自身的实现).但是有的时候,希望这里的顺序是可控的,此时线程等待就是一种办法.这里的线程等待,主要就是控制线程结束的先后顺序
一种常见的逻辑: t1线程,创建t2, t3, t4, 让这三个新的线程来分别执行一些任务. 然后t1线程最后在这里汇总结果.这样的场景就需要t1的结束时机必须比t2, t3, t4都迟

在这里插入图片描述

/**
 *线程等待
 */
public class TestDemo13 {
    
    
    public static void main(String[] args) {
    
    
        //创建两个线程t1
        Thread t1 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                int count = 0;
                while(count < 5){
    
    
                    count++;
                    System.out.println("线程运行中..");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("线程运行结束!");
            }
        };
        t1.start();

        try {
    
    
            //Thread.sleep(7000);
            System.out.println("join 执行开始:");
            t1.join();
            System.out.println("join 执行结束!");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

代码执行过程:执行start 方法的时候,就会立刻创建出一个新的线程来.同时main这个线程也立刻往下执行,就执行到t1.join .但是执行到t1.join的时候就发现,当前t1线程还是在运行中…
只要t1在运行中, join方法就会一直阻塞等待. 一直等到t1线程执行结束(run执行完了)

获取当前线程引用

public static Thread currentThread();

返回当前线程对象的引用(如果都是继承Thread的语法下创建的的线程,同this功能一样)

public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
    }
}

休眠当前线程

Sleep这个方法,本质上是把线程PCB给从就绪队列,移动到了阻塞队列~

public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒

public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

public class ThreadDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

线程的状态(六个状态)

用于辅助系统对于线程进行调度的属性

NEW: Thread对象创建出来了,但是内核的PCB还没创建出来.
RUNNABLE:当前的PCB也创建出来了,同时这个PCB随时待命(就绪). 这个线程可能是正在 CPU上运行,也可能是在就绪队列中排队
TIMED_ WAITING: 表示当前的PCB在阻塞队列中等待呢(这样的等待是一个"带有结束时间"的等待.)

WAITING:线程中如果调用了wait方法,也会阻塞等待.此时处在WAITING状态(死等)除非是其他线程唤醒了该线程.
BLOCKED:线程中尝试进行加锁,结果发现锁已经被其他线程占用了.此时该线程也会阻塞等待.这个等待就会在其他线程释放锁之后,被唤醒.

TERMINATED:表示当前PCB已经结束了. Thread对象还在.此时调用获取状态,得到的就是这个状态.

//关注 NEW 、RUNNABLE 、TERMINATED 状态的转换
public class ThreadStateTransfer {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(() -> {
    
    
            for (int i = 0; i < 1000_0000; i++) {
    
    
            }
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());;
        t.start();
        while (t.isAlive()) {
    
    
            System.out.println(t.getName() + ": " + t.getState());;
        }
        System.out.println(t.getName() + ": " + t.getState());;
    }
}

//关注 WAITING 、BLOCKED 、TIMED_WAITING 状态的转换
public static void main(String[] args) {
    
    
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                while (true) {
    
    
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                System.out.println("hehe");
            }
        }
    }, "t2");
    t2.start();
}
//修改上面的代码, 把 t1 中的 sleep 换成 wait
public static void main(String[] args) {
    
    
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                try {
    
    
				// [修改这里就可以了!]
				// Thread.sleep(1000);
                    object.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }, "t1");
//使用 jconsole 可以看到 t1 的状态是 WAITING
}
//yield() 大公无私,让出 CPU
Thread t1 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        while (true) {
    
    
            System.out.println("张三");
            // 先注释掉, 再放开
            // Thread.yield();
        }
    }
}, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            while (true) {
    
    
                System.out.println("李四");
                }
            }
        }, "t2");
    t2.start();
}
可以看到:
1. 不使用 yield 的时候, 张三李四大概五五开的数量
2. 使用 yield, 张三的数量远远少于李四
结论:
yield 不改变线程的状态, 但是会重新去排队.(java中用的很少)

多线程带来的的风险-线程安全 (重点)

/**
 * 线程安全
 */
public class ThreadDemo15 {
    
    
    public static class Counter{
    
    
        public int count = 0;
        public void increase(){
    
    
            count++;
        }
    }

    public static Counter counter = new Counter();

    public static void main(String[] args) {
    
    
        //此处建立两个线程,自增5万次
        Thread t1 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 50000; i++) {
    
    
                    counter.increase();
                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 50000; i++) {
    
    
                    counter.increase();
                }
            }
        };
        t2.start();

        try {
    
    
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

结果是个随机数,而且不是我们想要的100000这个数值

在这里插入图片描述

在这里插入图片描述

刚才的代码中,两个线程并发的自增了5w次.可是这5w次里面,有多少次触发了类似于上面的"**线程不安全问题",是不确定的.**最终自增结果是多少,也就不确定.但是可以知道的是最终结果, 一定是5w到10w之间的数据.
极端情况下:
如果每次自增都触发了线程安全问题(都是这样并列执行的) ,结果就正好是5w
如果每次自增都没触发线程安全问题(都是串行执行的) ,结果就正好是10w

产生线程不安全的原因(五种):

1.线程之间是抢占式执行的.

[根本原因,线程不安全的万恶之源]
抢占式执行,导致两个线程里面操作的先后顺序无法确定.
这样的随机性,就是导致线程安全问题的根本所在. [我们无力改变,操作系统内核实现的]

2.多个线程修改同一个变量

[这个和咱们代码的写法密切相关]

一个线程修改同一个变量:没有线程安全问题!不涉及并发,结果就是确定.
多个线程读取同一个变量:也没有线程安全问题!!读只是 单纯的把数据从内存放到CPU,不管怎么读,内存的数据始终不变(注意是读取而不是修改)
多个线程修改不同的变量:也没有线程安全问题!!其实就认为就类似于第一个情况.

所以为了规避线程安全问题,就可以尝试变换代码的组织形式,达到一个线程只改一个变量(有的场景下确实能这样变化.但是有的场景下不能这样变化.)

3.原子性.

++这样的操作,本质上是三个步骤,是一个"非原子"的操作.
=操作,本质上就是一个步骤,认为是一个"原子"的操作
所以上述代码咱们的++操作本身不是原子的.可以通过加锁的方式,把这个操作变成原子的

4.内存可见性

可见性指: 一个线程对共享变量值的修改,能够及时地被其他线程看到.

上述代码中线程2循环执行很多次自增,很多次自增就会涉及到很多次LOAD和SAVE操作;但是CPU执行ADD操作的速度比执行LOAD和SAVE要快1w倍.这个时候,线程2为了能够算的更快,为了提高程序的整体效率,于是线程2就会把中间的一些LOAD和SAVE操作省略掉.(简称编译器的优化) 这个省略操作是编译器(javac) 和JVM (java)综合配合达成的效果.如果只是单线程下,这样的优化,没有任何副作用.但是如果是多线程下,另外一个线程也尝试读取或者修改这个数据,此时的数据就不准确了

简而言之:多线程中一个线程修改, 一个线程读取由于编译器的优化,可能把一些中间环节的SAVE和LOAD操作去掉了.此时读的线程可能读到的是未修改过的结果.
volatile关键字,就是用来解决这个问题

5.代码重排序

也是CPU以及编译器的优化,再保持逻辑不发生变化情况下对代码一种优化

解决线程不安全问题

把上述代码中的++变成原子性(synchronized修饰)

synchronized的功能本质上就是,把"并发"变成"串行”

synchronized public void increase(){
    
    
    count++;
}
 //方法2:修饰代码块,如果该方法是非静态方法需要显式当前的加锁对象this
//因为Java中任意的对象,都可以作为"锁对象".
    synchronized (this){
    
    
            count++;
          }

如果两个线程同时并发的尝试调用这个synchronized修饰的方法
此时一个线程会先执行这个方法,另外一个线程会等待.等到第一个线程方法执行完了之后, 第二个线程才会继续执行.
就相当于**“加锁"和"解锁”**.进入synchronized修饰的方法,就相当于加锁.出了synchronized 修饰的方法,就相当于解锁.
如果当前是已经加锁的状态,其他的线程就无法执行这里的逻辑,就只能阻塞等待.

synchronized 关键字(监视器锁monitor lock)

synchronized 的特性

1)互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.(类似于公共卫生间显示牌上的 “有人/无人”).
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
synchronized用的锁是存在Java对象头里的。

在这里插入图片描述

注意:上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

2)刷新内存

synchronized 的工作过程: 获得互斥锁 -> 从主内存拷贝变量的最新副本到工作的内存 ->执行代码 -> 将更改后的共享变量的值刷新到主内存 -> 释放互斥锁 (所以synchronized 也能保证内存可见性.)

上述代码中由于编译器会优化代码效率,把中间的一些LOAD和SAVE省略掉,加上synchronized之后,就会禁止上面的优化,保证每次进行操作的时候都会把数据真的从内存读,也真的写回内存中.但是这也会让程序跑的慢一点,来能够保证数据算的准

3)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;(可以连续加锁两次而不出现编译报错).也叫可重入锁.Java语言的synchronized内部记录了当前这个锁是哪个线程持有的.再次加锁时会跳过直接执行下面的代码

synchronized public void increase(){
    
    
    synchronized (this){
    
    
        count++;
    }
}
//这个也是increase()加锁两次
static class Counter {
    
    
    public int count = 0;
    synchronized void increase() {
    
    
        count++;
    }
    synchronized void increase2() {
    
    
        increase();
    }
}

在可重入锁的内部, 包含了 "线程持有者" 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

synchronized 使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

synchronized修饰普通方法的话:这个时候如果两个线程并发的调用这个方法,此时是否会触发锁竞争,就看实际的锁对象是否是同一个了.
synchronized修饰静态方法的话:相当于针对类对象进行加锁.由于类对象是单例的,两个线程并发调用该方法, 则一定会触发锁竞争

1)直接修饰普通方法: 锁的 Demo 对象

public class Demo {
    
    
    public synchronized void methond() {
    
    
    }
}

2)修饰静态方法: 锁的 Demo 类的对象

public class Demo {
    
    
    public synchronized static void method() {
    
    
    }
}

3)修饰代码块: 明确指定锁哪个对象.

//锁当前对象
public class Demo {
    
    
    public void method() {
    
    
        synchronized (this) {
    
    
        }
    }
}
//锁类对象
public class Demo {
    
    
    public void method() {
    
    
        synchronized (Demo.class) {
    
    
        }
    }
}

上述代码Demo.class叫做反射.反射也是面向对象中的一个基本特性(和封装继承多态是并列关系).反射也叫"自省"
程序运行时,这个对象里包含哪些属性,每个属性叫啥名字,是啥类型public/private 包含哪些方法,每个方法叫啥名字,参数列表是啥, public/private...这些信息来自于.class 文件(java被编译生成的二进制字节码)会在JVM运行的时候加载到内存中就通过"类对象"来描述这个具体的.class文件的内容.
通过类名.class就得到了这个类对象.特点:每个类的类对象都是单例的.

Java 标准库中(集合类)的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用,最新版本Java16里已经弃用了)
其中Stack(栈)继承自Vector.
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer

还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
String

volatile 关键字

一般来说,如果某个变量,在一个线程中读, 另一个线程中写,这个时候大概率需要使用volatile

volatile的功能是保证内存可见性,但是不能保证原子性.(synchronized是两者都可以保证)

import java.util.Scanner;
/**
 * volatile 关键字
 */
public class ThreadDemo17 {
    
    
    static class Counter{
    
    
        public int flag = 0;
    }

    public static void main(String[] args) {
    
    
        Counter counter = new Counter();
        Thread t1 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                while (counter.flag == 0){
    
    
                    //假设执行操作
                }
                System.out.println("循环结束!");
            }
        };
        t1.start();

        Thread t2 = new Thread(){
    
    
            @Override
            public void run() {
    
    
                //用户输入数来改变flag的值
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数: ");
                counter.flag = scanner.nextInt();
            }
        };
        t2.start();
    }
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

在这里插入图片描述

static class Counter{
    
    
    //一旦给这个flag加上volatile之后,此时后续的针对flag的读写操作,就都能保证一定是操作内存
    volatile public int flag = 0;
}

再说内存可见性

在这里插入图片描述

JVM针对计算机的硬件结构,又进行了一层抽象. (主要就是因为Java要考虑到跨平台,要能支持不同的计算)
JVM就把CPU的寄存器, L1, L2, L3 cache统称为"工作内存”(一般指的不是真的内存)
JVM也把真正的内存称为"主内存"

如图三个线程,就有各自的工作内存(每个线程都有自己独立的上下文,独立的上下文就是各自的一组寄存器/cache的内容)
CPU在和内存交互的时候,经常会把主内存的内容,拷贝到工作内存,然后进行操作,再写回到主内存.
这个过程中就非常容易出现数据不一致的情况,这种情况在编译器开启优化的时候会特别严重.

volatile或者synchronized就能够强制保证接下来的操作是操作主内存.
在生成的java字节码中强制插入一些"内存屏障"的指令.这些指令的效果,就是强制同步主内存和工作中的内存的内容

猜你喜欢

转载自blog.csdn.net/TuttuYYDS/article/details/125340210