最近也是将多线程和线程安全这部分又回顾了一下,算是javase中的内容,这部分需要理解,以后如果学习分布式中的juc并发编程时基础必须打好,希望大家也都能不断进步,找到好工作吧。

多线程

什么是CPU

CPU 的中文名称是中央处理器,是进行逻辑运算用的,主要由运算器、控制器、寄存器三部分组成,从字面意思看就是运算器就是起着运算的作用,控制器就是负责发出 cpu 每条指令所需要的信息,寄存器就是保存运算或者指令的一些临时文件,这样可以保证更高的速度。 也就是我们的线程运行在 cpu 之上。 如果CPU很差,即使内存再大也很慢

线程和进程

进程是资源分配最小单位,线程是程序执行的最小单位。 计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。 总结:进程是资源分配最小单位,线程是程序执行的最小单位

什么是进程:

  1. cpu从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程

  2. 一个程序如果被cpu多次被读取到内存中,则变成多个独立的进程 什么是线程: 线程是程序执行的最小单位,在一个进程中可以有多个不同的线程同时执行,每个线程可以独立负责一个模块,即使其中一个报错,也互不影响,实现并行操作。

进程中需要线程是为了 方便同一个应用程序(进程)更好并行处理/操作

比如一个应用程序需要同时监听、渲染、保存,如果只有一个线程则只能干一件事,不能同时处理,实现并行操作

使用多线程原因:

  1. 提高程序运行效率

  2. 必须使用,比如开发文本编辑器必须要求同时执行

并行/串行区别:

串行也就是单线程执行,代码执行效率非常低,代码从上向下执行;如果上方出错下方就不会运行。

并行就是多个线程(模块)并行一起执行,效率比较高。

注意: 在执行多个线程时依赖CPU进行时间片分配,实际上单核CPU并不是真正的多线程,因为一次只能执行一个线程(通过上下文切换来执行线程,每个线程的就绪和运行也会来回变),也正因如此线程并不是越多越好

使用多线程一定提高效率吗?

不一定,需要了解 cpu 调度的算法 就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务

上下文切换太频繁就会影响性能,而且一般服务器核数8核则最多开启8个线程,否则多个线程竞争CPU,频繁切换肯定效率低。

应用场景

  1. 客户端(移动 App 端/)开发;

  2. 异步发送短信/发送邮件

  3. 将执行比较耗时的代码改用多线程异步执行;

  4. 异步写入日志 日志框架底层

  5. 多线程下载(文件、视频等)

同步与异步的区别

同步概念:就是代码从上向下执行。 串行

异步的概念:单独分支执行 相互之间没有任何影响。 并行

例如: http请求本身是同步,如果等不到消息就会一直阻塞,所以我们可以将耗时的部分开多线程异步执行来进行优化,减少阻塞时间和吞吐量

多线程的创建方式

注意线程执行完毕就正常死亡

1)继承 Thread 类创建线程

Thread.sleep(3000)也不影响主线程,并且主线程和子线程执行顺序和快慢也是依赖CPU调度,是随机的,注意直接调用run那么就是单线程了,必须start启动

2)实现 Runnable 接口创建线程

3)使用匿名内部类的形式创建线程

这种就不用类刻意去实现Runnable,哪个方法需要多线程就写,这样更灵活一些

// 2.使用匿名内部类的形式创建线程
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "<我是子线程>");
    }
}).start();

4)使用 lambda 表达式创建线程

new Thread(() -> System.out.println(Thread.currentThread().getName() + "<我是子线程>"), "我是新名字").start();

可以指定名字,不过一般默认的thread-0,1,2就足够好了

5)使用 Callable 和 Future 创建线程 (可以拿到线程的返回结果)

底层用到了juc并发包的阻塞LockSupport.park()和唤醒LockSupport.unpark()

6)使用线程池例如用 Executor 框架

底层使用到了队列

7)spring @Async 异步注解

线程安全

例子

多线程同时对同一个全局变量做写操作(只有修改才会产生,多个线程读没影响),可能会受到其他线程的干扰,就会发生线程安全性问题。

全局共享变量存储在堆内存

注意: 当竞争CPU从就绪到运行状态然后再操作时,此时就容易发生线程安全问题,而如果俩线程一直处于运行状态会小概率发生线程安全问题

public class ThreadCount implements Runnable {
    private int count = 100; // 公共变量
​
    /**
     * 如何保证线程一直在运行状态 死循环控制
     */
    @Override
    public void run() {
        while (true) {
            if (count > 1) {
                try {
                    // 运行状态----休眠状态--cpu的执行权让给其他的线程
                    // 休眠----先变成就绪,然后竞争CPU变运行状态
                    Thread.sleep( millis: 30); // 加上这段代码才大概率发生安全问题
                } catch (Exception e) {
                    // 每次运行休眠来回换竞争肯定大概率出问题
                }
                /**
                * 错误做法,可能发生线程安全问题
                * count--;
                * System.out.println(Thread.currentThread().getName() + "," + count);
                */
                // 正确做法
                synchronized (this){
                    count--;
                    System.out.println(Thread.currentThread().getName() + "," + count);
                }
            }
        }
    }
​
    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
}

解决(实现同步)

注意 不同对象肯定经过synchronized才上锁,如果单纯new一个对象不经过肯定也没获取到锁

核心思想: 给可能发生线程安全的代码上锁(同一个jvm中多个线程竞争锁的资源) 分布式锁(多个jvm中竞争锁的资源)

最终只能有一个线程能够获取到锁,哪个线程能获取到,就可以执行到该代码

注意加锁的范围,如果直接 public synchronized(非公平锁) void run() 则会让多线程变成单线程(因为上述例子是进入后就死循环不释放锁) 如果没有获取锁成功 中间需要经历锁的升级过程,一直没有获取到锁就一直阻塞等待

juc并发编程: 锁有很多类型,比如: 重入锁 悲观锁 乐观锁 公平锁 非公平锁

加锁缺点: 可能影响程序执行效率

synchronized(翻译是同步)

原理: 底层jvm是用c语言写的 获取锁和释放锁都是底层虚拟机jvm实现好了,包括唤醒等,如果使用lock,我们还要手动处理好逻辑

  1. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁。

synchronized(对象锁) // 可以是this,或者自己指定一个对象
{
  需要保证线程安全的代码
}

注意: 只能拦截一个this对象的不同线程, 即每个对象各有一个锁

ThreadCount threadCount1 = new ThreadCount();
ThreadCount threadCount2 = new ThreadCount(); // 此时创建俩个对象,则拦截不生效,仍然出现线程安全问题(数据重复了)
new Thread(threadCount1).start();
new Thread(threadCount2).start();

优化方法:

private Object objectLock = new Object(); 然后synchronized(objectLock)

private String lock = "lock"; 也ok

即可解决上述的问题

  1. 修饰实例方法,作用于当前实例方法加锁,进入同步代码前要获得 当前实例 的锁

  • 实例方法默认是this锁(谁调就会上锁),即必须是同一个线程对象才能保证安全,此时写法比第一条写法简单了一些

public void run() {
    while (true) {
        try {
            //运行状态----休眠状态--cpu的执行权让给其他的线程
            // 休眠----运行状态
            Thread.sleep(millis: 30);
        } catch (Exception e) {
        }
        cal();
    }
}
​
// 加在实例方法上,默认是this锁
public synchronized void call() {
    if (count > 1) {
        count--;
        System.out.println(Thread.currentThread().getName() + "," + count);
    }
}
  1. 修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码前要获得 当前类对象 的锁

  • 因为静态方法只有一份,所有类.class刚好也一份可以使用

public static synchronized void call() {
    if (count > 1) {
        count--;
        System.out.println(Thread.currentThread().getName() + "," + count);
    }
}
----------------等价----------------
public static void call() {
    synchronized(ThreadCount.class){
        if (count > 1) {
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }
    }
}
  • 加锁位置一般不在死循环外加应该就没事,因为防止不释放锁变成单线程

死锁问题

例子: 一个线程先拿到自定义lock锁还需要对象锁,而另一个线程则相反,此时如果它们各拿一个,那么就会同时等另一个线程释放,结果都不放手就发生了死锁

所以尽量避免使用嵌套锁, 如果不确定可以在jdk安装的bin目录中启动jconsole.exe找到对应的模块,然后需要在启动时进行检测即可

        if (count % 2 == 0) {
            synchronized (lock) {
                a();
            }
        } else {
            synchronized (this) {
                b();
            }
        }
​
public synchronized void a() {
    System.out.println(Thread.currentThread().getName() + ",a方法...");
}
​
public void b() {
    synchronized(lock){
        System.out.println(Thread.currentThread().getName() + ",b方法...");
    }
}

面试: 线程如何实现同步? -- 如何保证线程安全性问题

使用 synchronized 锁,JDK1.6 开始 锁的升级过程 偏向锁→轻量级锁→重量锁(尽量不使用,因为效率低) 使用 Lock (JUC并发包) 锁 ,需要自己实现锁的升级过程。底层是基于 aqs + cas 实现 (依赖操作系统互斥指令,竞争锁会发生用户态到内核态切换.可能影响效率) 使用 Threadlocal,需要注意内存泄漏的问题 原子类 CAS 非阻塞式

springmvc接口注意线程安全

需要注意: Spring MVC Controller 默认是单例的 所以需要注意线程安全问题 单例的原因有二: 1、为了性能。 2、不需要多例。 @Scope (value = "prototype") 设置为多例子。

不过一般也不在方法前加,因为会影响效率,只有可能发生安全问题时再加上即可

@Scope("prototype") // 可以设置非单实例,这样我们的count就不会共享,因为每次用一个都创建新对象,所以锁也多个,然后就不阻塞了
public class CountService {
​
    private int count = 0;
​
    /**
     * spring 默认 bean 对象都是单例
     * @return
     */
    @RequestMapping("/count")
    public synchronized String count() {
        try {
            log.info(">count<" + count++);
            try {
                Thread.sleep(millis: 3000);
            } catch (Exception e) {
            }
        } catch (Exception e) {
​
        }
        return "count";
    }
}

等待 / 通知机制

等待/通知的相关方法是任意 Java 对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object上(方便拿取对象锁),方法如下:

  1. notify ():通知一个在对象上等待的线程,使其从 main () 方法返回,而返回的前提是该线程获取到了对象的锁

  2. notifyAll ():通知所有等待在该对象的线程

  3. wait ():调用该方法的线程进入 WAITING 状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用 wait () 方法后,会释放对象的锁 。 (释放锁资源,同时阻塞当前线程)

  4. wait()和wait(0)是一样的,表示无限等待直到被另一个线程唤醒

wait和notify必须结合synchronized锁使用,需要放在里边,所以单独出现在main中肯定也报错,没获取到锁

public class Test01 {
    private Object objectLock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Test01().print();
    }
​
    public void print() throws InterruptedException {
        synchronized (objectLock) {
            System.out.println(">1<");
            /**
             *          this.wait();释放锁资源 同时当前线程会阻塞
             *          this.wait()、notify 结合到 synchronized
             */
            // 获取到锁的对象.wait
            objectLock.wait(); // 用自定义锁就要用自定义.wait,如果this锁则this(不能new Test01) 因为此时相当于重新new一个对象,而且这个对象没获取到锁就释放肯定不行
            System.out.println(">2<"); // 阻塞等待后这段代码不会执行
        }
    }
}

唤醒:

注意一般不写在一起,因为让主线程唤醒,而不能自己唤醒自己

public void print() throws InterruptedException {
        new Thread(new Runnable(){
            synchronized (objectLock) {
            System.out.println(Thread.currentThread().getName() + ">1<");
            try{
              objectLock.wait();
            }catch (Exception e){
              e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ">2<");
        }).start();
        try {
            Thread.sleep(3000);
            // 主线程3s 之后唤醒该子线程
            synchronized (objectLock) {
                objectLock.notify(); // 也是放在synchronized中才行
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
}

生产者与消费者问题

正常就是一输入一输出

个人理解:

其实就是需要一个flag标记变量,然后生产和消费都需要先获取同一把锁,然后判断flag,如果没有轮到自己就wait释放锁,否则就执行代码完毕后再通知一下另一个线程即可(因为一个线程wait之后就阻塞了,所以另一个线程通知后才能唤醒,而且唤醒写在最后边也比较合理); 如果唤醒写在前边判断中,那么因为阻塞之后就不动了,所以另一个线程执行后不符合flag也wait释放锁,但是另一个没有被唤醒,所以应该没线程执行了,死锁

Join方法

回顾: 这种线程写法都比较灵活,可以将需要的内容包装,然后在main中try-catch或者线程中也可以

private Object object = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread01 thread01 = new Thread01();
    thread01.print();
    try {
        // 主线程 阻塞3s
        // 唤醒子线程
        Thread.sleep(millis: 3000);
        thread01.notifyThread();
    } catch (Exception e) {
​
    }
}
​
public void notifyThread() {
    synchronized (object) {
        object.notify();
    }
}
​
public void print() throws InterruptedException {
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (object) {
                // 主动释放this 锁 同时当前主线程变成了阻塞状态
                try {
                    System.out.println(Thread.currentThread().getName() + ",1");
                    object.wait();
                    System.out.println(Thread.currentThread().getName() + ",2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

join方法:

可以让线程按顺序执行,底层使用了synchronized this锁和

public class Thread02 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",线程执行"), "t1"); // 未锁
        // t2需要等待t1执行完毕
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t1.join(); // 应该是t1调用了,t1 wait就相当于t2wait了,然后上边t1执行完,自动释放唤醒所有包含t1锁的部分,t2再继续执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ",线程执行");
            }
        });
        // t3需要等待t2执行
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t2.join(); // 等待底层唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ",线程执行");
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

Join 底层原理是基于 wait 封装的,唤醒的代码在 jvm Hotspot 源码中(应该用C++写的)。当 jvm 在关闭线程之前会检测阻塞在 t1 线程对象上的线程,然后执行 notfyAll (), 这样 t2 就被唤醒

public static void main(String[] args) throws InterruptedException {
    Thread06 thread06 = new Thread06();
    Thread thread = thread06.print();
    thread.start();
    try {
        Thread.sleep(millis: 3000);
        // 中断线程
        thread.interrupt(); // 底层自动唤醒  线程处于wait()/sleep()/join()才可中断
    } catch (Exception e) {
    }
}
​
public Thread print() {
    Thread thread = new Thread(() -> {
        synchronized (object) {
            System.out.println("1");
            try {
                object.wait(timeout: 0); // 阻塞等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("2");
        }
    });
    return thread;
}

多线程底层7种状态

线程刚开始处于新建状态(就绪状态),然后调用start方法,此时不会立即变运行(因为CPU调度也是有调度算法的)

其实超时和等待的区别就是是否传递了时间

守护线程与用户线程

java 中线程分为两种类型:用户线程和守护线程。通过 Thread.setDaemon (false) 设置为用户线程;通过 Thread.setDaemon (true) 设置为守护线程。如果不设置次属性,默认为用户线程。
​
1. 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如jvm中的垃圾回收线程。
2. 用户线程是独立存在的,不会因为其他用户线程退出而退出。

守护线程,可以让自己main主线程只要结束,那么子线程不管是否运行完都挂掉 也是用Thread接受一下thread.setDaemon(true);

默认是用户线程, 并行

安全停止线程

调用 stop 方法 (现在不行了)
Stop: 中止线程,并且清除监控器锁的信息,但是可能导致 线程安全问题,JDK 不建议用。
Destroy: JDK 未实现该方法。
Interrupt(线程中止)
​
Interrupt 打断正在运行或者正在阻塞的线程。
1. 如果目标线程在调用 Object class 的 wait ()、wait (long) 或 wait (long, int) 方法、join ()、join (long, int) 或 sleep (long, int) 方法时被阻塞,那么 Interrupt 会生效,该线程的中断状态将被清除,抛出 InterruptedException 异常。
2. 如果目标线程是被 I/O 或者 NIO 中的 Channel 所阻塞,同样,I/O 操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。

不过像循环中断以后还是尽量考虑如何进行完整流程时中断,要不然线程执行一半终端容易出问题

public void run(){
 while(true){
   System.out.println("执行");
   Thread.sleep(millis: 100000);
   System.out.println("未执行");
 }
}
​
public static void main(String[] args) {
    Thread09 thread09 = new Thread09();
    thread09.start();
    try {
        Thread.sleep(millis: 3000); // 这次休眠主要是让main线程有点参与感
    } catch (Exception e) {
    }
​
    System.out.println("<<中断子线程>,>");
    // 中断 阻塞或者正在运行的线程
    thread09.interrupt(); // 注意,如果子线程没有sleep此时无法终端
}
if (this.isInterrupted()) {
  // 加上这个判断就不用sleep了
  break;
}
再拓展一下
private volatile boolean isStart = true;
boolen
 while(isStart){
}
// 这样就可以手动控制了 volatile也要加,主要是保证线程可进性

Lock锁(注意要写好防止安全问题)

在 jdk1.5 后新增的 ReentrantLock 类同样可达到此效果,且在使用上比 synchronized 更加灵活 相关 API: 使用 ReentrantLock 实现同步 lock () 方法:上锁 unlock () 方法:释放锁 使用 Condition 实现等待 / 通知 类似于 wait () 和 notify () 及 notifyAll () Lock 锁底层基于 AQS 实现,需要自己封装实现自旋锁。

Synchronized --- 属于 JDK 关键字 底层属于 C++ 虚拟机底层实现 Lock 锁底层基于 AQS 实现 -- 变为重量级 Synchronized 底层原理 --- 锁的升级过程

注意: 还是推荐synchronized(自动),因为lock(手动)必须自己处理好锁的升级过程

public class Thread10 {
    private Lock lock = new ReentrantLock(); // 手动
​
    public static void main(String[] args) {
        /*
         * Lock 获取锁 和释放锁 需要开发人员自己定义
         */
         Thread10 t1 = new Thread10();
         t1.print1();
         try{
           Thread.sleep(1000);
         }catch(Exception e){}
         Thread10 t2 = new Thread10();
    }
    
    public void print1() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // 获取锁
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
            } catch (Exception e) {
            } finally {
                lock.unlock();  // 这才是正常的,因为线程执行完应该自动释放锁,要不然另一个线程无法获取,一直阻塞等待
            }
        }
    }, "t1").start();
  }
  public void print2() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // 获取锁
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取锁成功");
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        }
    }, "t2").start();
  }
}

线程终止(结束)应该自动释放锁,强制中断(底层也会释放锁并可以通知自己从wait到唤醒状态(如果是运行就不能被中断,而且不会唤醒其他线程)

notify的随机由 JVM 等待集的实现决定与CPU调度无关(应该只负责使线程变运行状态吧)
public class Thread11 {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition(); // 区分synchronized的wait等方法,而且不能单纯出现
​
    public static void main(String[] args) {
        Thread11 thread11 = new Thread11();
        thread11.cal();
        try {
            Thread.sleep(millis: 3000);
        } catch (Exception e) {}
        thread11.signal();
    }
    
    public void signal() {
    try {
        lock.lock();
        condition.signal(); // 相当于随机唤醒一个拿锁的线程
    } catch (Exception e) {} finally {
        lock.unlock();
      }
    }
    
    public void cal() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock(); // 也是要有锁才能await,注意不要使用wait、notify等,这个是Object也就是synchronized自带的
                System.out.println("1");
                condition.await(); // 主动释放锁 同时当前线程变为阻塞状态
                System.out.println("2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 最终也要释放
            }
        }
    }).start();
  }
}

多线程 yield

主动释放 cpu 执行权

  1. 多线程 yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程。

  2. 具体的实现依赖于底层操作系统的任务调度器,所以不一定能真正执行

public class Thread12 extends Thread {
    public Thread12(String name) {
        super(name);
    }
​
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (i == 30) {
                System.out.println(
                    Thread.currentThread().getName() + ",释放cpu执行权"
                );
                this.yield();
            }
            System.out.println(
                Thread.currentThread().getName() + "," + i
            );
        }
    }
​
    public static void main(String[] args) {
        new Thread12("mayikt01").start();
        new Thread12("mayikt02").start();
    }
}

核心仍然看操作系统的调度算法

了解一下即可

public class Thread13 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int count = 0;
            for (;;) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t1线程:");
        Thread t2 = new Thread(() -> {
            int count = 0;
            for (;;) {
                System.out.println(Thread.currentThread().getName() + "," + count++);
            }
        }, "t2线程:");
        t1.setPriority(Thread.MIN_PRIORITY); // 此时运行count会相对小一些
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

Join/Wait 与 sleep 之间的区别

本质都可以让运行变等待,不过区别很大

sleep (long) 方法在睡眠时不释放对象锁 (不需要synchronized代码块,可单独存在) join (long) 方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于jvm、wait执行 Wait (long) 方法在等待的过程中释放对象锁

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐