基本概念

程序

程序就是你一行一行敲出的代码 , 它们在没有运行之前都是死的.

进程

当程序进行运行 , 原来死的代码就开始跑起来 , 从开始运行到全部结束 , 这就是一个进程

线程

代码执行每一条并列任务时 , 都是一个线程的运行 , 一个进程中存在一个或多个线程 , 

并行与并发

操作系统分为多核cpu和单核cpu , 当只有一个cpu运行代码中多个线程时 , 此时就是并发 ; 多核cpu一对一处理线程 , 此时就是并行.

多线程中cpu并发运行 , 形成时间切片 , 在不同线程之间调度 , 调度的顺序是随机的 , 这和线程自身是否满足运行条件也有关系

临界区与锁

程序中需要访问的公共资源(文件 , 全局变量)就是临界区 , 是防止多个线程同时访问临界区的一个限制 , 当一个线程访问临界区时,就会获得锁 , 其他线程只有等到当前线程释放锁才能进行访问

wait()与notify()方法

wait(): 程序主动进行调用 , 当一个线程的运行条件无法满足时会调用此方法进行等待 , 其他线程则继续竞争cpu
notify(): 程序主动进行调用 , 当一个线程运行时条件满足时 , 等待的线程就会开始执行

注意 : cpu调度线程是随机被动进行的 , 而wait()与notify()方法是程序主动进行调度 , 结合使用能加快运行效率

多线程方式

一个线程从建立到结束的过程:

创建线程的方式有三种: 

  1. 继承Tread类
  2. 实现Runnable接口
  3. 通过Callable和Future创建线程

继承Thread类

Thread类封装了操作系统调用的复杂性 , 具有标准化的线程操作 .

当自定义类没有继承任何父类的时候 , 我们通过自定义类继承Thread父类 , 并调用父类的start()方法进入就绪状态

下面是创建子类的代码:

package 线程;

public class MythreadDemo extends Thread{
    private String threadName;
    public MythreadDemo(String name) //创建构造器
    {
        this.threadName = name;
        System.out.println("Creating " + threadName);
    }
    
    @Override
    public void run() //重写run方法
    {   
        for(int i=1;i<=100;i++)
        {
            if(i%2==0)
            {
                System.out.println(threadName+":"+i);
            }
        }
    }
}

在子类中创建私有属性和构造器声明线程的成功创建;

  private String threadName;

    public MythreadDemo(String name)//创建构造器

    {

        this.threadName = name;

        System.out.println("Creating " + threadName);

    }

进入执行状态 : 通过调用父类的Run()方法使线程进入执行状态 , 由于Tread父类的Run()方法是空的 , 所以我们需要重写方法 , 写入我们要执行的代码.

@Override//声明重写父类的方法

    public void run()//重写run方法

    {

        for(int i=1;i<=100;i++)

        {

            if(i%2==0)

            {

                System.out.println(threadName+":"+i);

            }

        }

    }

重写的注意点 : 

(1)方法名必须相同

(2)参数列表必须相同

(3)返回值类型必须相同

下面是主线程中新建线程的代码:

package 线程;

public class ThreadExample {
    public static void main(String[] args) {
        MythreadDemo thread1= new MythreadDemo("线程1"); 
        thread1.start();
        MythreadDemo thread2=new MythreadDemo("线程2");
        thread2.start();
        for(int i=0;i<3;i++)
        {
            System.out.println("好的好的");
        }
    }
}

新建状态:  新建两个子类的实例对象(thread1和thread2)

MythreadDemo thread1 = new MythreadDemo("线程1");

MythreadDemo thread2 = new MythreadDemo("线程2");

就绪状态 :分别调用父类的start()方法 , 线程加入cpu的竞争

thread1.start();
thread2.start();

主线程执行的代码:

for(int i=0;i<3;i++)

        {

            System.out.println("好的好的");

        }

实现 Runnable 接口

Runnable是Thread类实现的一个储存执行代码的接口 , 和继承Thread类不同的是 , 实现接口将执行操作和执行内容分隔开

下面是Thread类的源码:

public class Thread implements Runnable {
    private Runnable target;  // 传入的Runnable对象
    
    // 构造器:接收Runnable任务
    public Thread(Runnable target) {
        this.target = target;
    }
    
    // Thread自己的run方法
    @Override
    public void run() {
        if (target != null) {
            target.run();  // 调用传入的Runnable对象的run方法
        }
        // 如果target为null,就什么也不做(需要子类重写)
    }
}

Thread类通过构造器接收传入的对象.

private Runnable target;  // 传入的Runnable对象
    
    public Thread(Runnable target) {
        this.target = target;
    }

Thread类实现了Runnable接口 , 进而实现Run()方法

    @Override
    public void run() {
        if (target != null) {
            target.run();  
        }

        //初始执行内容为空

}

由于Thread类构造器只允许传入Runnable接口的实例对象 , 所以我们必须通过实现接口创建对象

下面是具体实现的代码:

package 线程;

public class MyRunnable implements Runnable{     
        private String RunnableName;
        public  MyRunnable(String name)
        {
            this.RunnableName=name;
            System.out.println("Creating:"+RunnableName);
        }

    @Override
    public void run()
     {
        
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 1) {
                System.out.println(RunnableName+":" + i);
            }
        }
     }
}

创建具体类实现Runnable接口:

public class MyRunnable implements Runnable{     
        private String RunnableName;
        public  MyRunnable(String name)
        {
            this.RunnableName=name;
            System.out.println("Creating:"+RunnableName);
        }

在新建类中实现接口的Run()方法:

@Override
    public void run()
     {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 1) {
                System.out.println(RunnableName+":" + i);
            }
     }

创建具体类的实例对象:

MyRunnable Runnable1 = new MyRunnable("线程1");

MyRunnable Runnable2 = new MyRunnable("线程2");

将Runnable类型的实例对象传入Tread类:

Thread thread1 = new Thread(Runnable1);

Thread thread2 = new Thread(Runnable2);

再通过Tread类下的对象调用start()方法,创建线程:

thread1.start();

thread2.start();

注意:

  1. 不能使用具体类的对象直接调用run()方法 , 那样只是在主线程里同步执行两个任务 , 必须通过Thread类调用start方法才能成功创建线程 , 并异步执行两个任务
  2. run( )方法是由 Java 虚拟机和操作系统底层调用的,不是由你的应用程序代码直接调用的

通过 Callable 和 Future 创建线程

下面是实现Callable接口的子类代码:

package 线程;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String>{
    public String Callname;

    public MyCallable(String name){
        this.Callname=name;
    }

    //实现call方法
    public String call() throws Exception{//带有返回值供Futrue.get()方法调用
        //线程具体的执行任务
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 1) {
                System.out.println(Callname + ":" + i);
            }
        }
        // 返回输出结果
        String result="输出结果:"+Callname;
        return result;
    }
}

创建子类实现接口:

public class MyCallable implements Callable<String>

实现Callable接口中的call方法(带有返回值):

 public String call() throws Exception{

        return result;

}

注意:实现的call()方法可以抛出异常也可以得到返回值 (这也是运用callable接口创建线程的独特之处)

下面进行main方法创建线程:

package 线程;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executor=Executors.newFixedThreadPool(2);

        // 创建线程
        MyCallable tast1=new MyCallable("线程1");
        MyCallable tast2=new MyCallable("线程2");

        // 将线程导入submit方法
        Future <String> Future1=executor.submit(tast1);
        Future <String> Future2=executor.submit(tast2);

        // 获取返回值
        try {
            String result1=Future1.get();
            System.out.println("线程1:"+result1);
            String result2=Future2.get();
            System.out.println("线程2:" + result2);
            
        } catch (Exception e) {
            System.out.println("执行发生错误");
        }
        // 主线程输出
        System.out.println("okok");
        // 关闭线程池资源
        executor.shutdown();
    }
}

创建固定线程容量的线程池(executors类中的方法全为静态方法):

ExecutorService executor=Executors.newFixedThreadPool(2);

创建MyCallable类对象实例(此处同理于继承Thread类 , 实现Thread接口创建对象):

MyCallable tast1=new MyCallable("线程1");
MyCallable tast2=new MyCallable("线程2");

进入就绪状态(类似于Thread.start()方法):

Future <String> Future1=executor.submit(tast1);
Future <String> Future2=executor.submit(tast2);

注意:

  1. start()方法在executor内部自动进行调用 , 我们只需手动调用submit传入我们的实例对象即可
  2. submit()方法的返回值为call()方法中的返回的值
  3. 运用泛型Futrue<String>接收返回值

用try-catch语句包装Futrue调用方法等操作:

try {
            String result1=Future1.get();
            System.out.println("线程1:"+result1);
            String result2=Future2.get();
            System.out.println("线程2:" + result2);
            
        } catch (Exception e) {
            System.out.println("执行发生错误");
        }

注意:

  1. 可通过Futrue.get()方法获取call()方法中的返回值来判断执行任务的状态
  2. Futrue.get()会抛出受检异常 , 必须运用try-catch进行异常处理

main方法中进入就绪状态内部原理:

main线程提交任务:
executor.submit(task1)
    ↓
线程池把Callable包装成FutureTask
    ↓  
线程池创建/复用Thread来执行FutureTask
    ↓
Thread执行FutureTask.run()方法
    ↓
FutureTask.run()内部调用: task1.call()
    ↓
call()方法执行,返回结果"XXX"
    ↓
FutureTask把结果保存起来: result = "XXX"
    ↓
main线程调用: future1.get()
    ↓
如果call()还没完成,main线程等待
    ↓
call()完成后,main线程拿到结果"XXX"

先把call()方法包装在run()方法中 , 再执行run()方法

底层原理还是运用Tread类进行创建线程和执行任务 ! ! !

线程异常处理(公共资源)

临界区(即公共资源)只能被一个线程访问,当多个线程同时访问公共资源时,就会导致资源无法及时更新,导致执行结果异常.

所有我们需要给临界区定义锁(Lock) , 当线程需要访问时 , 要先拿到锁 , 此时其他线程无法同时访问 , 访问完毕之后自动释放锁 (获取锁是各线程竞争的结果)

给临界区设置锁有三种方式:

  1. 使用synchronized修饰符定义方法(隐式锁)
  2. 等价的手动同步(使用this)
  3. 使用专门的锁对象

方式一:
通过创建实例类形成隐式锁:

// 方法1:使用synchronized修饰符
    public synchronized void increment() {
        count++;
    }

下面是运用synchronized修饰符定义方法的代码:

package 线程;

//隐式锁实例类
 class  Counter{
    //定义公共资源
     public int x = 0;
     //创建隐式锁方法(使用公共资源的核心代码)
     public synchronized void count(){
         x++;
     }
     //实时获取变量数值
    public int getValue(){
        return x;
     }
}
//执行类
public class ShareResouce1 extends Thread{
    //定义Counter类类型的变量
    public Counter counter;
    String Lockname;
    //构造器:传入创建Counter实例对象和线程名
    public ShareResouce1( Counter counter ,String name)
    {
        //对成员名字赋值
        this.counter=counter;
        this.Lockname=name;
    }
    //run方法书写执行内容
    public void run(){
        while(counter.getValue()<100)
        {
            //竞争隐式锁的关键代码(决定了执行和阻塞状态的变换)
            counter.count();//调用Counter类的隐式锁方法
            System.out.println(Lockname + ":" + counter.getValue());
        }
    }
}

运用synchronized修饰符创建隐式锁:

public synchronized void count(){
         x++;
     }

等同于:

public void method() {
    // 自动获取this对象的钥匙
    synchronized(this) {
        x++;
    }
    // 自动归还钥匙
}

count包裹的内容为需要使用公共资源的代码块

实时返回变量x的数值:

public int getValue(){
        return x;
     }

定义Counter类的变量 , 用于存储主线程传入的实例对象:

public Counter counter;
    String Lockname;
    //构造器:传入创建Counter实例对象和线程名
    public ShareResouce1( Counter counter ,String name)
    {
        //对成员名字赋值
        this.counter=counter;
        this.Lockname=name;
    }

调用实例类中的隐式锁方法:

counter.count();//调用Counter类的隐式锁方法
System.out.println(Lockname + ":" + counter.getValue());

重点:是否竞争到锁是进入运行状态和阻塞状态的关键

新建(New) → 启动 → 就绪(Runnable) 
                    ↓
              竞争synchronized锁
                    ↓
            成功 → 运行(Running) → 执行完毕 → 终止(Terminated)
                    ↓
            失败 → 阻塞(Blocked)
                    ↓
           锁释放 → 就绪(Runnable)

注意:只将需要访问临界区(即公共资源)的执行代码放入隐式锁的方法中 , 其余代码无需放入 , 从而避免传入传出的不便

下面是相应的main主线程代码:

package 线程;

public class ShareResouce1Example {
    public static void main(String[] args) {
        //创建Counter类的共享对象(只可创建一个实例对象,保证锁的唯一性)
        Counter count1=new Counter();
        //传入同一个共享对象
        ShareResouce1 task1=new ShareResouce1(count1,"线程1");
        ShareResouce1 task2=new ShareResouce1(count1,"线程2");
        System.out.println("线程1开始执行");
        task1.start();
        System.out.println("线程2开始执行");
        task2.start();

    }
}

方法二:

使用This对象锁

this对象锁: 每一个创建的对象都有一个锁机制 , 指向该对象.

为了确保锁的唯一性 , 我们只能创建一个实例对象 , 但由于线程有多个 , 所以我们只能使用实现Runnable接口的方式创建线程.

下面是运用this锁实现公共资源锁的代码:

package 线程;

public class ShareResouce3 implements Runnable{
    //公共资源共享
    public static int x=0;
    public String Sharename;
    public ShareResouce3(String name){
        this.Sharename=name;
    }

    public void run(){
        while(x<100){
            //运用this对象锁
            synchronized (this) {
                x++;
            }
            //输出创建Thread对象的线程名
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}

重点:运用this对象锁时必须要注意只能创建一个实例对象(确保锁的唯一性)

main主线程:

package 线程;

public class ShareResouce3Example {
    public static void main(String[] args) {
        ShareResouce3 tast=new ShareResouce3("公共资源");

        Thread tast1=new Thread(tast,"线程1");
        System.out.println("线程1开始执行");
        tast1.start();

        Thread tast2 = new Thread(tast, "线程2");
        System.out.println("线程2开始执行");
        tast2.start();
    }
}

只创建一个实例对象,而创建多个线程对象,从而保证this的唯一性和多线程的创建.

方式三:

创建专用Lock对象

下面是包装Lock锁和线程类的代码:

package 线程;

//封装类(放置公共资源变量和创建静态对象)
class makeLock
{
    //定义公共资源静态变量
    public static int x = 0;
    public static final Object Lock=new Object();//公共锁对象(final保证锁唯一性)
}

public class ShareResouce2 extends Thread{
    public String Myname;
    
    public ShareResouce2(String name){
        this.Myname=name;
    }
    public void run(){
        while(makeLock.x<100)
        {
            //通过封装类调用静态锁
            synchronized(makeLock.Lock){
                makeLock.x++;
            }
            System.out.println(Myname+":"+makeLock.x);
        }
    }
}

创建类封装资源和唯一锁对象:

class makeLock
{
    //定义公共资源静态变量
    public static int x = 0;
    public static final Object Lock=new Object();//公共锁对象(final保证锁唯一性)
}

在run方法中调用封装类的资源和锁对象:

public void run(){
        while(makeLock.x<100)
        {
            //通过封装类调用静态锁
            synchronized(makeLock.Lock){
                makeLock.x++;
            }
            System.out.println(Myname+":"+makeLock.x);
        }

注意:

  1. 无法在线程类中创建实例锁对象 , 会导致每个线程都会得到一个锁对象 , 无法处理访问异常
  2. 也不建议使用静态锁对象 , 创建资源管理类来放置公共资源和锁对象最稳妥.

线程通信(此板块敬请期待)

wait()方法

notify()方法

重要内容!!!

文章到这里就暂告一段落啦,真心希望能为你带来一些启发和帮助。❤️

你的每一次阅读,都是我坚持分享的最大动力 ✨。如果文中有任何不清楚、或者你觉得有误的地方,非常欢迎在评论区指出来 🤔,我们一起探讨,共同进步~

如果觉得内容不错,别忘了点个赞 👍 让我知道哦!

关注我 👉,不要错过系列后续的更多干货分享!期待在下一篇文章里,再次与你相遇。😊

Logo

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

更多推荐