多线程学习(基础篇)(代码片段)

3ERROR(s) 3ERROR(s)     2022-12-02     548

关键词:

文章目录

一、多线程概述

为什么要有线程呢?
首先并发已经成为了现在变成的刚需,虽然进程也可以实现并发编程,但是线程相比于进程更清轻量,它创建,销毁,调度线程都要比进程快。

1.进程和线程之间的关系

  • 进程是一个应用程序,线程是一个进程中的执行单元。
  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 一个进程可以启动多个线程,同一个进程中的多个线程之间可能共享了一些资源。
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

在java语言中,线程A和线程B的堆内存和方法区内存是共享的,栈内存独立不共享,一个线程一个栈。,之所以引进多线程机制,就是为了提高程序的处理效率。

2.创建线程的方式

方式一:显式继承Thread,重写run方法来指定线程的执行代码。

public class ThreadDemo5 
    public static void main(String[] args) 
        myThread t=new myThread();//创建myThread实例
        t.start();
    

class myThread extends Thread
    @Override
    public void run()
        System.out.println("多线程分支");
    

方式二:匿名内部类继承Thread,重写run方法来指定线程的执行代码。

Thread t1=new Thread()
            @Override
            public void run()
                int a=0;
                for (int i = 0; i < count; i++) 
                    a++;
                
            
        ;

方式三:显式实现Runnable接口,重写run方法。

class myRunable implements Runnable
    @Override
    public void run() 
        System.out.println("多线程分支2");
    

创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.

Thread t = new Thread(new MyRunnable());

方式四:匿名内部类创建 Runnable 子类对象

 Thread tttt =new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("匿名内部类Runnable创建 Thread 子类对象");
            
        );

3.Thread的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了

4.中断一个线程

通过共享的标记来进行

public class ThreadDemo3 

    public static boolean isQuit = false;
    public static void main(String[] args) throws InterruptedException 
       //温和版本
        Thread t = new Thread() 
            @Override
            public void run() 
                while(!isQuit)
                    System.out.println("正在嘟嘟嘟");
                    try 
                        Thread.sleep(300);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
                System.out.println("结束了");
            
        ;
        t.start();
        Thread.sleep(5000);
        System.out.println("终止交易");
        isQuit=true;
    

调用 interrupt() 方法

public class ThreadDemo4 
    public static void main(String[] args) throws InterruptedException 
        //暴力版本
        Thread t =new Thread()
          @Override
          public void run()
              while(!Thread.currentThread().isInterrupted())
                  System.out.println("正在嘟嘟嘟");
                  try 
                      Thread.sleep(300);
                   catch (InterruptedException e) 
                      e.printStackTrace();
                      break;
                  
              
              System.out.println("嘟嘟嘟结束了");
          
        ;
        t.start();
        Thread.sleep(1000);
        System.out.println("终止交易");
        t.interrupt();
    

  • Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志

5.获取当前线程引用

线程默认命名规则为:Thread-0;Thread-1…
获取对象信息包括:获取线程对象,获取线程名字,修改对象名字。

public class ThreadDemo6 
    public static void main(String[] args) 
        
        Thread tt=Thread.currentThread();
        System.out.println(tt.getName());
        tt.setName("HEHE");
        System.out.println(tt.getName());
    

获取线程对象的方法使用currentThread(),这是一个静态方法,作用是获取当前对象的引用。若此方法在main中,只会现实除main中的线程,即主线程,其名字就叫做"main"

获取线程名字,修改对象名字:

public class ThraeadDemo6 
    public static void main(String[] args) 
        Thread t=new Thread();
        System.out.println(t.getName());
        t.setName("HEHE");
        System.out.println(t.getName());
    

6.线程的状态

  • NEW: Thread对象有了,内核中的线程(PCB)还没有。安排了工作, 还未开始行动
  • RUNNABLE: 就绪状态,可工作的,当先线程在CPU上或者随时上CPU,有一个专门的就绪队列来维护。
  • BLOCKED: 阻塞状态 这几个都表示排队等着其他事情
  • WAITING: wait 这几个都表示排队等着其他事情
  • TIMED_WAITING: 超时等待
  • TERMINATED: 内核中的线程已经结束了(PCB没了),但是Thread对象还在(需要GC来回收)
    isAlive表示线程存活,除了NEW和TERMINATED之外其余状态都是isAlive。

二、线程安全

1.演示线程不安全

/*
* 测试线程安全
*
*/


public class ThreadDemo7 
    public static class Counter 
        public int count=0;
        public void increase()
            count++;
        
    

    public static void main(String[] args) throws InterruptedException 
        Counter counter=new Counter();
        Thread t=new Thread()
            @Override
            public void run()
                for (int i = 0; i < 50000; i++) 
                    counter.increase();
                
            
        ;
        t.start();


        Thread tt=new Thread()
            @Override
            public void run()
                for (int i = 0; i < 50000; i++) 
                    counter.increase();
                
            
        ;
        tt.start();
        t.join();
        tt.join();
        System.out.println(counter.count);
    


正常来说结果应该是100000,但是测试之后发现结果如下:



我们发现每次测试结果都不相同且并不是100000,原因如下

  • 线程是抢占式调度
  • 自增操作不是原子的(1.把内存的数据独到CPU。2.把CPU当中的数据自增一。3.再把CPU当中的数据写回到内存中。)
  • 多个线程尝试修改同一个变量。
  • 内存可见性导致的线程安全问题
  • 指令重排序

2.如何避免线程安全问题

对症下药!
(1).抢占式调度(这个没办法解决,操作系统内核实现)
(2).自增操作非原子性(给自增操作加锁)

这里加上关键字synchronized加锁,加锁之后同一时刻只有一个线程能获得锁,如果其他线程也尝试获得锁,就会陷入阻塞状态,直到刚才的锁释放,此时剩下的线程再重新竞争锁,也就是说加了锁把原本并行的线程强制改成了串行,再次运行后结果为100000。

3.synchronized关键字

synchronized关键字:进入方法前先尝试加锁,方法结束后自动解锁,这样的好处就是避免忘记解锁的情况。如果加锁的时候锁被占用了,该代码就会阻塞等待,直到前面的锁被释放,才能获取到这个锁。

synchronized的几种常见用法:

  • 加在普通方法前:表示锁this。
  • 加到静态方法前:表示锁当前类的类对象。
  • 加到某个代码块之前:显示指定给某个对象加锁。

特性1:互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

特性2:刷新内存
synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

特性3:可重入
按照之前的设定,只有当第一个锁被释放之后才能获得第二个锁,但是释放第一个锁是由第一个线程来完成的,如果第一个线程开始摆烂,那么这个锁将无法打开,也就造成了死锁。
这样的锁被称为不可重入锁
而synchronized是可重入锁在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁。

理解锁具体细节:

  • 工作流程:多个线程同时获取同一把锁只有一个线程可以获取到,其他线程会阻塞等待(BLOCKDE),两个线程分别尝试获取两把不同的锁, 不会产生竞争。
  • 底层实现:每个对象都有一个对象头,头里面有一个锁标记,所以它势必要搭配一个具体的对象来使用
  • 加锁的时候一定要明确到底是给哪个对象加锁。

4.Java 标准库中的线程安全类

这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

以下为加了锁的线程安全的类

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

三、volatile关键字

写入volatile修饰的变量的时候

  • 改变线程CPU寄存器中volatile变量副本的值。
  • 将改变之后副本的值从CPU寄存器中刷新到主内存中。

读取volatile修饰的变量的时候

  • 从主内存中读取volatile修饰的变量最新值到CPU寄存器当中。
  • 从CPU寄存中读取volatile变量的副本。

访问工作内存(CPU寄存器或者CPU缓存)的速度远远快于访问主内存,但是有可能数据不一致,而加上了volatile之后强制读写主内存,虽然速度慢了但是不会出错。

代码示例:

/**
 * @ClassName UseVolatile
 * @Description 这是用来测试volatile关键字的测试代码
 *              正常情况下线程2输入数据完毕之后线程1应该结束,但是实际情况是这样嘛?
 * @Author Rcy
 * @Data 2022/1/11 0:37
 */
public class UseVolatile 

        static class A
            public  int flg=0;
        

        public static void main(String[] args) 
            A a=new A();
            Thread thread=new Thread()
                @Override
                public void run()
                    while(a.flg==0)

                    
                    System.out.println("结束了");
                
            ;
            thread.start();


            Thread thread2=new Thread()
                @Override
                public void run()
                    Scanner sc=new Scanner(System.in);
                    System.out.println("请输入一个数字:");
                    a.flg=sc.nextInt();

                
            ;
            thread2.start();
        
    


我们测试了很多此发现此时线程1并没有结束!这是因为线程1当中的flag读取的一直都是CPU寄存器当中的值,我们的线程二将主内存当中的值修改了并没有影响到线程1。这是系统优化的负面效果,我们要消除这种优化,所以我们想到了使用volatile关键字修饰flag让他每次从内存中读取数值,保证了内存可见性 (一个线程读,一个线程写,可能修改操作对于读线程没有生效)

修改后的代码如下:

static class A
        public volatile int flg=0;
    

测试结果如下:

volatile虽然能保证内存可见性,但是它不能保证原子性。

四、wait()和notify()方法

1.wait() 方法:

作用:

  • (1)让其加入等待队列,释放当前的锁
  • (2) 等待接收通知
  • (3)收到唤醒通知,从新尝试获取锁

如果notify()发送通知在1,2之间,可能会出现竞态条件问题(错过了通知,导致登了一辈子也没等明白),所以wait()中的1,2是原子性的。

wati()结束条件:

  • 其他线程调用该对象的notify()方法
  • wait等待超时(timeout参数控制)
  • 其他线程调用该等待线程的interrupted方法

这里需要注意的是,如果被interrupted的线程没有start则等待状态不会恢复

2.notify()方法:

  • 通知等待该对象对象锁的其他线程,对其发出通知,让他们从新获得该对象锁。
  • 若有多个线程等待随机选一个。
  • 不会立即释放该对象锁 ,必须等到同步代码块执行完。

3.为什么wati() 方法和notify()方法需要synchronized一起使用?

防止错过notify()发送的通知,造成永远阻塞等待。

每一个对象都有一个监视器,监视器当中有有一个锁和一个阻塞队列和同步队列,因为wait()阻塞的线程放在阻塞队列中,因为竞争失败则放在同步队列当中,notify()则是把阻塞队列的线程放到同步队列当中去。

4.wait()和sleep()方法比较

相同点:

  • 都可以让线程阻塞一段时间

不同点:

  • wait()需要搭配synchronized使用,sleep不需要。
  • wait()是Object的方法,sleep()是Thired的静态方法。

五、单例模式讲解

之前专门写过一篇介绍单例模式模式的文章:

单例模式讲解

六、阻塞队列

阻塞队列是什么?
阻塞队列是一种特殊的队列,也遵循先进先出的的原则,它是一种线程安全的数据结构,他有以下一些特点:

  • 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候,继续出队列就会阻塞,知道有其他线程往队列中插入元素。

1.借助标准库实现生产者消费者模型

生产者消费者模型就是通过一个容器解决生产者和消费者之间的强耦合问题。
生产者和消费者之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产出来数据之后不需要等消费者处理,直接丢给阻塞队列,消费者也不问生产者要东西,直接在阻塞队列中取!

  • BlockingQueue实际是一个接口,具体实现的类是LinkedBlockingQueue。
  • put用于阻塞式入队列,take用于阻塞式出队列。
  • BlockingQueue也有offer,poll,peek方法,但是不带阻塞性质。
/**
 * @ClassName ThreadQueue
 * @Description 标准库版生产者消费者模型
 * @Author Rcy
 * @Data 2022/3/8 16:28
 */
public class ThreadQueue 
    public static void main(String[] args) throws InterruptedException 
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
        //put和offer都可以入队,put有阻塞的作用

        //生产者
        Thread producer = new Thread()
            @Override
            public void run() 
                for (int i = 0; i <10000 ; i++) 
                    try 
                        blockingQueue.put(i);
                        System.out.println("生产了元素"+i);
                        sleep(1000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ;
        producer.start();
       
        //消费者
        Thread customer = new Thread()
            @Override
            public void run() 
                while(true)
                    try 
                        Integer cur= blockingQueue.take();
                        System.out.println("消费元素"+cur);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ;
        customer.start();
        
        customer.join();
        producer.join();
    

2.阻塞队列的实现

  • 使用循环队列的方式来实现阻塞队列
  • 使用synchronized关键字进行加锁控制

put唤醒take的阻塞,take唤醒put的阻塞

put插入元素,如果队列满了就要进入wait等待(由take方法来唤醒) (这里注意要在while中等待,因为被唤醒的时候有可能同时唤醒了多个线程,这里要用while再次进行判断)

take取出元素的时候,判断队列是否为空,为空就进行wait等待 (在while中进行等待,原因同上)

import java.util.Queue;

/**
 * @ClassName BlockingQueue
 * @Description TODO
 * @Author Rcy
 * @Data 2022/3/8 18:05
 */


public class WriteBlockingQueue 
    static class BlockingQueue 
        private int[] elem = new int[1000];
        int start = 0;
        int tail = 0;
        int size = 0;

        private void put(int item) throws InterruptedException 
            //输入队列满了 就要开始阻塞了
            synchronized (this) 
                if(elem.length==size)
                   this.wait();
                
                //入队列,把新的元素放到数组的尾部
                elem[tail]=item;
                //数组尾部++
                tail++;
                //如果超过或等于尾部就从0开始
                while(tail>=elem.length) 
                    tail = 0;
                
                size++;
                this.notify();
            
        

        //出队列
        private int take() throws InterruptedException 
            //要阻塞了
            int ret=0;
            synchronized (this) 
                while(size==0)
                    this.wait();
                
                ret = elem[start];
                //相当于出队列
                start++;
                //判断当前start大小超过或等于就从0开始计数
                if(start>=elem.length)
                    start=0;
                
                size--;
                this.notify();
            
            return ret;
        查看详情  

java多线程(基础篇)(代码片段)

...文记录于阿里一群大佬们手码的书:《深入浅出Java多线程》线程与进程的区别:进程是一个独立的运行环境,而线程是在进程中执行的一个任务进程单独占有一定的内存地址空间,所以进程间存在内存隔离,... 查看详情

工作流(代码片段)

前言前面学习了很多多线程和任务的基础知识,这里要来实践一下啦。通过本篇教程,你可以写出一个简单的工作流引擎。本篇教程内容完成是基于任务的,只需要看过笔者的三篇关于异步的文章,掌握C#基础,即可轻松完成。C... 查看详情

多线程基础篇(代码片段)

线程之间方法区和堆内存共享,栈内存不共享;哪个线程调用sleep()方法,哪个线程就进入睡眠状态,与哪个对象调用该方法无关.start()方法的作用是创建一个线程的栈内存,该方法与普通方法相同,执行完立刻销毁.packagetest1;publicclassRacer... 查看详情

多线程基础学习(代码片段)

...实验仅证明优先级设置的比较高并不一定先执行, *线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。 *线程的优先级具有继承性,比如A线程启动... 查看详情

号称史上最全java多线程与并发面试题总结—基础篇(代码片段)

前言 多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括... 查看详情

python3之多线程基础学习(代码片段)

参考资料python3多线程官网多线程优点多线程类似于同时执行多个不同程序,多线程运行有如下优点:1、使用线程可以把占据长时间的程序中的任务放到后台去处理。2、用户界面可以更加吸引人,比如用户点击了一个按钮去触发... 查看详情

多线程(基础篇1)

  在多线程这一系列文章中,我们将讲述C#语言中多线程的相关知识,在多线程(基础篇)中我们将学习以下知识点:创建线程中止线程线程等待终止线程确定线程的状态线程优先级前台线程和后台线程向线程传递参数使用C#... 查看详情

手把手教你做项目多线程篇——基础知识详解(代码片段)

多线程导读项目中多线程的目的实战操作小知识创建一个简单的线程守护线程主进程等待子进程结束共享全局变量的特性锁互斥锁信号量送点资源导读随着暑假的推进,手把手教你做项目前边的准备也差不多了后续的项目也... 查看详情

浅谈多线程(代码片段)

...重要。还是一样首先从学习三步曲来开始:1、什么是多线程?  既然要探讨什么是多线程,那么我们从字面上来理解这个词,多线程翻译过来也就是多个线程。讲到这里就需要了解到线程。这时就有存在疑问,什么是线程?... 查看详情

多线程(基础篇1)转载

在多线程这一系列文章中,我们将讲述C#语言中多线程的相关知识,在多线程(基础篇)中我们将学习以下知识点:创建线程中止线程线程等待终止线程确定线程的状态线程优先级前台线程和后台线程向线程传递参数使用C#的lock... 查看详情

尚硅谷_java零基础教程(多线程)--学习笔记(代码片段)

Java多线程一、基本概念1、程序、进程、线程2、单核CPU和多核CPU、并行与并发3、使用多线程的优点二、线程的创建和使用1、API中创建线程的两种方式1.1、方式一:继承Thread类1.2、方式二:实现Runnable接口1.3、Thread类的调... 查看详情

多线程基础必要知识点!看了学习多线程事半功倍(代码片段)

...的日子要努力一点才行!只有光头才能变强回顾前面:多线程三分钟就可以入个门了!Thread源码剖析本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基础的。要是能好好理解这些基础,... 查看详情

多线程学习一(多线程基础)(代码片段)

前言   多线程、单线程、进程、任务、线程池...等等一些术语到底是什么意思呢?到底什么是多线程?它到底怎么用?我们一起来学习一下多线程的处理如何理解  进程:进程是给定程序当前正在执行的实例(... 查看详情

java_多线程并发编程基础篇—thread类中start()和run()方法的区别(代码片段)

1.start()和run()的区别说明start()方法: 它会启动一个新线程,并将其添加到线程池中,待其获得CPU资源时会执行run()方法,start()不能被重复调用。run()方法:它和普通的方法调用一样,不会启动新线程。只有等到该方法执行完... 查看详情

python基础学习(代码片段)

Python多线程多线程类似于同时执行多个不同程序,多线程运行有如下优点:使用线程可以把占据长时间的程序中的任务放到后台去处理。用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出... 查看详情

秒杀多线程第四篇一个经典的多线程同步问题(代码片段)

上一篇《秒杀多线程第三篇原子操作Interlocked系列函数》中介绍了原子操作在多进程中的作用,现在来个复杂点的。这个问题涉及到线程的同步和互斥,是一道非常有代表性的多线程同步问题,如果能将这个问题搞清楚,那么对... 查看详情

c#多线程(16):手把手教你撸一个工作流(代码片段)

...流构建器依赖注入实现工作流解析前言前面学习了很多多线程和任务的基础知识,这里要来实践一下啦。通过本篇教程,你可以写出一个简单的工作流引擎。本篇教程内容完成是基于任务的,只需要看过笔者的三篇关于异步的文... 查看详情