多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行的现象成为并发运行!

多线程的用途:

  1. 当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上”同时”运行
  2. 一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行

    线程的生命周期图

    image-20210401171503425.png

    创建线程方式

    继承Thread并重写run方法

    一个线程类,重写run方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。启动该线程要调用该线程的start方法,而不是run方法!!!

优点:

  • 在于结构简单,便于匿名内部类形式创建。

    缺点:

  • 直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的。

  • 定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起,不利于线程的重用。 ```java public class ThreadDemo1 { public static void main(String[] args) {
    1. //创建两个线程
    2. Thread t1 = new MyThread1();
    3. Thread t2 = new MyThread2();
    4. //启动线程,注意:不要调用run方法!!
    5. t1.start();
    6. t2.start();
    } }

class MyThread1 extends Thread{ public void run(){ for (int i=0;i<1000;i++){ System.out.println(“hello姐~”); } } }

class MyThread2 extends Thread{ public void run(){ for (int i=0;i<1000;i++){ System.out.println(“来了~老弟!”); } } }

<a name="Ywpqt"></a>
## 实现Runnable接口单独定义线程任务
(当需要代码复用的时候,优先选择这个)
```java
public class ThreadDemo2 {
    public static void main(String[] args) {
        //实例化任务
        Runnable r1 = new MyRunnable1();
        Runnable r2 = new MyRunnable2();
        //创建线程并指派任务
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}
class MyRunnable1 implements Runnable{
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("你是谁啊?");
        }
    }
}
class MyRunnable2 implements Runnable{
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("开门!查水表的!");
        }
    }
}

匿名内部类优化代码

public class ThreadDemo3 {
    public static void main(String[] args) {
        //匿名内部类
        Thread t1 = new Thread(){
            public void run(){
                for(int i=0;i<1000;i++){
                    System.out.println("你是谁啊?");
                }
            }
        };

        //Runnable可以使用lambda表达式创建
        Runnable r2 = ()->{
                for(int i=0;i<1000;i++){
                    System.out.println("我是查水表的!");
                }
        };
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

主线程

1.java中的代码都是靠线程运行的,执行main方法的线程称为”主线程”。 我们自己定义的线程在不指定名字的情况下系统会分配一个名字,格式为”thread-x”(x是一个数)。
2.static Thread currentThread()
该方法可以获取运行这个方法的线程

public class CurrentThreadDemo {
    public static void main(String[] args) {
        Thread main = Thread.currentThread();//获取执行main方法的线程(主线程)
        System.out.println("线程:"+main);
        dosome();//主线程执行dosome方法
    }
    public static void dosome(){
        Thread t = Thread.currentThread();//获取执行dosome方法的线程
        System.out.println("执行dosome方法的线程是:"+t);
    }
}
//线程:Thread[main,5,main]
//执行dosome方法的线程是:Thread[main,5,main]

多线程实现连接

原理图:

image-20210430152730779.pngimage.png

多客户端连接一个服务器代码,以及构造方法灵活传参

package socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

  private ServerSocket serverSocket;

  public Server(){
    try {
      System.out.println("正在启动服务端...");
      serverSocket = new ServerSocket(8088);
      System.out.println("服务端启动完毕");
   } catch (IOException e) {
      e.printStackTrace();
   }
 }

  public void start(){
    try {
      while(true) {
        System.out.println("等待客户端连接...");
        Socket socket =serverSocket.accept();
        System.out.println("一个客户端连接了!");
        //启动一个线程来负责与该客户端交互
        //1创建线程任务
        ClientHandler handler = newClientHandler(socket);
        //2创建一个线程并执行该任务
        Thread t = newThread(handler);
        //3启动该线程
        t.start();
     }
    } catch (IOException e) {
      e.printStackTrace();
   }
 }

  public static void main(String[] args){
    Server server = new Server();
    server.start();
 }
    //创建一个线程,这里以为要反复调用,所以使用内部类的方式
  private class ClientHandler implements Runnable{
      private Socket socket;
      //采用构造方法进行传参,这样在主线程里面new这个对象的时候,就把参数传进来走下面的run
    public ClientHandler(Socket socket){
      this.socket = socket;
   }
    public void run(){
      try{
        InputStream in =socket.getInputStream();
        InputStreamReader isr = newInputStreamReader(in,"UTF-8");
        BufferedReader br = newBufferedReader(isr);
        String line;
        while((line =br.readLine()) != null) {
          System.out.println("服务端说:"+line);
       }
     }catch(IOException e){
        e.printStackTrace();
     }
   }
 }
}

线程常用方法

    Thread main =Thread.currentThread();//获取主线程

    String name = main.getName();//获取线程的名字
    System.out.println("名字:"+name);

    long id = main.getId();//获取该线程的唯一标识
    System.out.println("id:"+id);

    int priority =main.getPriority();//获取该线程的优先级
    System.out.println("优先级:"+priority);

    boolean isAlive = main.isAlive();//该线程是否活着
    System.out.println("是否活着:"+isAlive);

    boolean isDaemon =main.isDaemon();//是否为守护线程
    System.out.println("是否为守护线程:"+isDaemon);

    boolean isInterrupted =main.isInterrupted();//是否被中断了
    System.out.println("是否被中断了:"+isInterrupted);

setPriority

线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片,线程调度器尽可能均匀的将时间片分配给每个线程,线程有10个优先级,使用整数1-10表示1为最小优先级,10为最高优先级,5为默认值调整线程的优先级可以最大程度的干涉获取时间片的几率,优先级越高的线程获取时间片的次数越多,反之则越少。

min.setPriority(Thread.MIN_PRIORITY);//将其设置为最小优先级
max.setPriority(8);//自定义设置大小

static void sleep(long ms)

线程提供的一个静态方法,使运行该方法的线程进入阻塞状态,持续传参的毫秒,超时后线程会自动回到runnable(可运行的)状态等待再次获取时间片并发运行。

    public static void main(String[] args){
    System.out.println("程序开始了!");
    try {
      Thread.sleep(5000);//主线程阻塞5秒钟
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
    System.out.println("程序结束了!");
 }

InterruptedException(打断异常)

viod interrupt()(打断)

sleep方法处理异常,当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.

 public static void main(String[] args){
     //线程一
    Thread lin = new Thread(){
      public void run(){
         System.out.println("林:刚美完容,睡一会吧...");
        try {
          Thread.sleep(50000000);
       } catch(InterruptedException e) {
          System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了相了!");
       }
        System.out.println("林:醒了!");
     }
   };
     //线程二
    Thread huang = new Thread(){
      public void run(){
        System.out.println("黄:大锤80,小锤40.开始砸墙!");
        for(int i=0;i<5;i++){
               System.out.println("黄:80!");
          try {
                Thread.sleep(1000);
         } catch(InterruptedException e) {
         }
       }
        System.out.println("咣当!");
        System.out.println("黄:搞定!");
        lin.interrupt();//走到这调用打断方法打断lin
     }
   };

    lin.start();
    huang.start();
 }//结果会提前终止lin的睡眠阻塞

setDaemon(booleanon)

守护线程也称为:后台线程 守护线程是通过普通线程调用setDaemon(booleanon)方法设置而来的,因此创建上与普通线程无异.守护线程的结束时机上有一点与普通线程不同,即当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程。所以,当一个进程里只要还有线程在跑,他就继续跑,它不保护任何线程,当所有线程停止,它也同时停止。

jack.setDaemon(true);//将jack设置为守护线程

void join()

  1. 线程提供了一个方法:该方法允许调用这个方法的线程在该方法所属线程上等待(阻塞),直到该方法所属线程结束后才会解除等待继续后续的工作.所以join方法可以用来协调线程的同步运行。
  2. 同步运行:多个线程执行过程存在先后顺序进行。
  3. 异步运行:多个线程各干各的,线程本来就是异步运行的。

    private static boolean isFinish =
    false;//标示图片是否下载完毕
    public static void main(String[] args){
    
     Thread download = new Thread(){
        public void run(){
         System.out.println("down:开始下载图片...");
         for(int i=1;i<=100;i++){
        System.out.println("down:"+i+"%");
           try {
             Thread.sleep(50);
          } catch(InterruptedException e) {
          }
        }
         System.out.println("down:图片下载完毕!");
         isFinish = true;
      }
    };
    
     Thread show = new Thread(){
       public void run(){
         try {
         System.out.println("show:开始显示文字...");
           Thread.sleep(3000);
         System.out.println("show:显示文字完毕!");
           //将show线程阻塞,等待download线程执行完毕再继续后续操作
          System.out.println("show:等待download下载图片...");
           download.join();//show开始阻塞,直到download执行完毕!
         System.out.println("show:等待完毕!");
         System.out.println("show:开始显示图片...");
           if(!isFinish){
             //当一个线程的run方法抛出一个异常,则线程会结束
             throw new RuntimeException("图片加载失败!");
          }
         System.out.println("show:图片显示完毕!");
        } catch(InterruptedException e) {
           e.printStackTrace();
        }
      }
    };
     download.start();
     show.start();
    }
    }
    

    多线程并发安全问题

    存在问题:

    当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪。

    临界资源:

    操作该资源的全过程同时只能被单个线程完成。相当于现实生活中多个人抢同一个东西导致的混乱。比如如下情况,抢到负数还在抢,直接跳过==0的判定了。 ```java public class SyncDemo1 { public static void main(String[] args){

    Table table = new Table(); Thread t1 = new Thread(){ public void run(){

     while(true){
        int bean =table.getBean();
       Thread.yield();
     System.out.println(getName()+":"+bean);
    }
    

    } };

    Thread t2 = new Thread(){ public void run(){

     while(true){
       int bean =table.getBean();
       Thread.yield();
     System.out.println(getName()+":"+bean);
    }
    

    } };

    t1.start(); t2.start(); } }

class Table{ private int beans = 20;//桌子上有20个豆子

public int getBean(){ if(beans==0){ throw new RuntimeException(“没有豆子了!”); } Thread.yield();//让线程主动放弃CPU时间,模拟执行到这里没有时间发生线程切换 return beans—;//会出现都到beans—之前,却都判定进来了,这时候会连续减2次,跳过beans==0 }

}

<a name="kTblf"></a>
## 并发出现问题的根源: 并发三要素
<a name="mr1ay"></a>
### 可见性: CPU缓存引起
> 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。看下面这段代码:

```java
//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;
  • 假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
  • 此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10. :::info 这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。 :::

    原子性: 分时复用引起

  1. 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  2. 经典的转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
  3. 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
  4. 所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

    有序性: 重排序引起

  5. 有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码: ```java int i = 0;
    boolean flag = false; i = 1; //语句1
    flag = true; //语句2

```

  1. 上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。
  2. 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
  3. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  4. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  5. 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
  6. 从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:1655710415(1).png
  7. 上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。