Java多线程和线程安全(详解)


多线程

一个java.exe至少有三个线程:main主线程,gc垃圾回收线程,还有一个异常处理线程(如果发生异常,会影响到主线程)

线程的相关API

.start():启动当前线程;在调用线程中的run()方法

.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中

.currentThread():静态方法,返回执行当前代码的线程

.getName():获取当前线程的名字

.setName():设置当前线程的名字

.Thread.currentThread().getName()获取当前线程的名字

.yield():主动释放当前线程的执行权yield(); Java线程中的Thread.yield()方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。

.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,原来未执行完线程才继续执行下去

.stop():强制结束当前线程(已过时)

.sleep(long millitime):线程休眠一段时间(规定时间内休眠)

.isAlive():判断当前线程是否存活

线程概念

进程

进程(process)是计算机的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位(操作系统运行的一个程序)

一个进程(都有独立的代码和数据空间;进程是资源分配的最小单位)可以有多个线程,每个线程(同一类线程共享代码和数据空间;线程是cpu调度的最小单位)都有独立的栈和程序计数器

线程

线程是进程的一个执行单元(一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支)

进程是线程的容器,一个进程至少有一个线程,也可以有多个线程

在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述等,每个线程都有各自的线程栈自己的寄存器环境自己的线程本地存储

主线程和子线程

JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程

Java线程不是孤立的,线程之间存在一些联系,如果a线程创建b线程,就称b线程为子线程,相反a就是b的父线程

串行(Sequential),并发(Concurrent),并行(parallel)

在这里插入图片描述

从硬件角度来说,如果单核cpu,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让cpu快速的在各个线程之间进行切换一个cpu同时执行多个任务(多个人同时做一件事,感觉是三个线程在同时执行)如果是多核心cpu,可以为不同的线程分配不同的cpu内核

线程创建与启动

java里面提供了一个java.lang.Thread类,那么一个类只要继承了此类就表示这个类为线程的主体类

线程的生命周期

在这里插入图片描述

新建状态

新建线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程

就绪状态

当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态

如果就绪状态的线程获取 CPU 资源,就可以执行 run()方法,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态

如果一个线程执行了sleep()(睡眠)suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态

同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)

死亡状态

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

线程创建有4种方式

一个是继承Thread()类、一个是实现Runnable接口、一个实现Callable接口,重写call()方法(有返回值)、最后一个则是线程池
在这里插入图片描述

一、继承一个Thread类的子类

  • 创建一个类去继承Thread类,重写抽象run()方法
  • 然后去类的实例化去调用start()方法
  • 创建线程实际调用的是父类Thread空参的构造器
//第一种定义一个Thread类的子类(扩展java.lang.Thread类)
public class SubClass extends Thread{
    
    
    //重写Thread父类中的run()方法
    //run()方法中的代码就是子线程要执行的任务
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+":"+"这是子线程run()方法");//Thread.currentThread().getName()表示当前线程名字
        //这里当前线程名字可以直接写成getName(),因为继承Thread类,所以可以直接调用
    }
}
public class Multithreading {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("JVM启动main线程,main线程执行main方法");
        //创建子线程对象
        SubClass p=new SubClass();
        SubClass p2=new SubClass();
        //启动线程
        p.setName("线程1");//设置线程名字
        p.start();
        p2.setName("线程2");//设置线程名字
        p2.start();
        /*
        调用线程start()方法来启动线程,启动线程的实质就是请求Java虚拟机运行相应的线程,这个线程具体在什么时候运行由线程调度器来决定
	注意:
	start()方法调用结束并不意味着子线程开始运行;
	新开启的线程会执行run()方法;
	如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序;
	多线程运行结果与代码执行顺序或调用顺序无关
		*/
    }
}
/*
输出结果:
JVM启动main线程,main线程执行main方法
线程2:这是子线程run()方法
线程1:这是子线程run()方法
*/
/*
注意:我们在启动线程的时候,实际上是调用start()方法,不能调用run()方法,因为这里实际上这里采用了设计模式中的模板方法模式,Thread类作为模板,而run()方法是在变化,如果调用run()方法,实际上是调用普通的run()方法,所以run()方法要放到子类来实现
*/

二、定义一个Runnable接口的实现类

  • 定义一个类实现该Runnable接口(先说一下java.lang.Runnable,它是一个接口,在它里面只声明了一个run()方法)

  • 实现接口的抽象run()方法,方法体就是线程所执行的内容

  • 创建实现类的对象,将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

  • 通过Thread类的对象去调用start()

//这里举了2个例子,第一个例子
package test;
public class Multithreading implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 1; i <=20; i++) {
    
    
            if(i%2==0){
    
    
                System.out.println(i);
            }
        }
    }
}
 class Text{
    
    
    public static void main(String[] args) {
    
    
        Multithreading p=new Multithreading();
        Thread p1=new Thread(p);
        p1.start();
        //这里等价于new Thread(new Multithreading()).start();
    }
}
/*
输出结果:
2
4
6
8
10
12
14
16
18
20
*/
//第二个例子
package test;
public class Bank {
    
    
    public static void main(String[] args) {
    
    
        new Thread(new A("A")).start();
        new Thread(new A("B")).start();
    }
}
class A implements Runnable{
    
    
    private String name;
    public A(String name) {
    
    
        this.name=name;

    }

    @Override
    public void run() {
    
    
        for (int i = 1; i <=10 ; i++) {
    
    
            if(i%2==0){
    
    
                System.out.println(name+":"+i);
            }
        }
    }
}
/*
输出结果:
A:2
A:4
B:2
B:4
A:6
B:6
B:8
A:8
B:10
A:10
*/

匿名子类去创建对象调用(另一种写法,用于线程只执行一次)

package test;
public class Text{
    
    
    public static void main(String[] args) {
    
    
        new Thread(){
    
       
            @Override
            public void run() {
    
    
                Thread.currentThread().setName("线程1");
                getPriority();
                for (int i = 0; i < 3; i++) {
    
    
                 System.out.println(Thread.currentThread().getName()+":"+i);
                    try {
    
    
                        sleep(1000);//线程睡眠:单位是毫秒1秒=1000ms
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                Thread.currentThread().setName("线程2");
                for (int i = 0; i < 3; i++) {
    
    
                 System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }).start();
        for (int i = 0; i < 3; i++) {
    
    
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
/*
输出结果:
main:0
线程2:0
线程2:1
线程1:0
线程2:2
main:1
main:2
线程1:1
线程1:2
*/

继承Thread类和实现Runnable接口的区别

如果一个类继承Thread,不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口优点

  • 适合多个相同的程序代码的线程去处理同一个资源

  • 可以避免java中的单继承的限制

  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

  • 线程池只能放入实现RunableCallable类线程,不能直接放入继承Thread的类

三、Callable接口(有返回值)

  • Thread类还是实现Runnable接口,Thread类是无法抛出更多的异常,实现Runnable接口是线程执行完毕之后并无法获得线程的返回值。Callable接口这种实现方式就可以完成我们的需求。这种方式的实现就是我们后面要详细介绍的Future 模式

  • 创建一个类实现Callable接口,实现call()方法。这个接口类似于Runnable接口,可以抛出异常和有返回值(支持泛型的返回值)(需要借助FutureTask类,比如获取返回结果)

  • Future接口可以对具体RunnableCallable 任务的执行结果进行取消、查询是否完成、获取结果等。FutrueTaskFutrue接口的唯一的实现类。FutrueTask同时实现了RunnableFuture接口,它既可以作为Callable被线程执行,又可以作为Future得到Callable的返回值

package test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Multithreading{
    
    
    public static void main(String[] args) {
    
    
        A p=new A();//创建Callable接口实现类的对象
        
        FutureTask p1=new FutureTask(p);//创建一个FutureTask,指定Callable对象,做为线程任务。(将此Callable接口实现类的对象作为传递到TutureTask构造器中,创建FutureTask的对象)
        new Thread(p1).start();
        
        try {
    
    
            //获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            
            Object sum=p1.get();//返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值
            System.out.println(sum);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }
}
//创建一个实现Callable的实现类
class  A implements Callable {
    
    
    //实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
    
    
        int sum=0;
        for (int i = 1; i <=100 ; i++) {
    
    
            if(i%2==0){
    
    
                sum+=i;
            }
        }
        return sum;
    }
}
//输出结果:2550

四、线程池

线程和数据库连接这些资源都是宝贵的资源。每次需要的时候创建,不需要的时候销毁,非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。线程池的好处是提高响应的速度(减少创建新线程的时间)降低资源消耗(重复利用线程池中线程)便于线程管理。JDK5.0提供线程池相关的API:ExecutorServiceExecutors

线程池的方法

corePoolSize:核心池的大小
maxmumPoolSize:最大线程数
KeepAliveTime:线程没有任务时最多保持多长时间后会终止
ExecutorService:真正的线程接口,常见子类ThreadPoolExecutor
    void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    <T>Future<T>submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
    void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    Executors.newFixedThreadPool(n):创建一个可重用的固定线程数的线程池
    Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
    Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期得执行
    补充
service.submit();//提交(适用于Callable,可以获取call返回值)
service.execute();//执行(适用于Runnable)
package test;

import test.Multithreading;

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

public class Bank {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService service = Executors.newFixedThreadPool(10);//创建线程池,提供指定数量的线程池
        service.execute(new A());//执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.shutdown();//关闭线程池

    }
}
class A implements Runnable{
    
    

    @Override
    public void run() {
    
    
        for (int i = 1; i <=10 ; i++) {
    
    
            if(i%2==0){
    
    
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
/*
输出结果:
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
*/

线程同步(解决线程安全问题)

线程安全概念

线程安全就是线程同步的意思,就是当一个程序对一个线程安全的方法或者语句进行访问的时候,其他的不能再对他进行操作了,必须等到这次访问结束以后才能对这个线程安全的方法进行访问。最常见的一种,多个线程处理共同数据的时候,比如线程1在进入方法后,拿到了Number的值,刚把这个值读取出来,还没有改变Number的值的时候,结果线程2也进来的,那么导致线程1和线程2拿到的Number值是一样的(简单来说就是线程运行的时候,别的线程也会参与进来)

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全(简单来书就是存在竞争的线程不安全,不存在竞争的线程就是安全的)

解决线程安全有以下几种方式

  • 当多个线程同时操作一个可共享的资源时会出现线程安全问题,将会导致数据不一致,因此使用同步锁来防止该操作执行完之前不许被其他线程执行,从而保证了该变量的唯一性和准确性
  • 我们可以通过同步机制解决线程问题(解决线程安全其核心:就是将某一个线程中的任务给锁(同步锁)起来,这个时候JVM就不得不等到本任务里的代码执行完以后在去执行另外一个线程里的任务)

synchronized关键字

synchronized()关键字(英译过来是同步的意思)就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,定义格式如下:

使用同步代码块解决实现Runnable接口线程安全问题

synchronized(同步监视器){
    
    
//需要同步的代码(操作共享数据的代码即为同步代码;共享数据:多个线程同步操作的变量)(多个线程必须要共同一把锁)(同步监视器:俗称锁。任何一个类的对象都可以充当锁)
}
//使用同步代码块解决Runnable线程安全问题
package test;
class  SaleWindow  implements  Runnable {
    
    
    private  int  ticket=  100 ;
    Object obj=new Object();
    @Override
    public void run() {
    
    
        while (true){
    
    
            synchronized (obj){
    
    
                //synchronized ()括号里面要写同步监视器,俗称:锁。任何一个类的对象都可以充当锁(这里也可以用this充当同步监视器,代表唯一的SaleWindow的对象)(也可以写成SaleWindow.class用当前类去充当对象(Class clazz=SaleWindow.class))
                if(ticket>0){
    
    
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":卖了"+ticket+"票");
                    ticket--;
                }else break;
            }
        }
    }
}
public class Secure{
    
    
    public  static  void  main(String[] args) {
    
    
        SaleWindow p =  new  SaleWindow();
        Thread p1 =  new  Thread(p);
        Thread p2 =  new  Thread(p);
        p1.setName( "窗口A" );
        p2.setName( "窗口B" );
        p1.start();
        p2.start();
    }
}
/*
注意:
操作共享数据的代码,即为需要被同步的代码
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
synchronized()锁的括号里的对象,而不是代码,其次,对于非静态的的synchronized的方法,锁的对象本身也就是this;当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。
*/

使用同步代码块解决继承Thread类的方式的线程安全问题

//使用同步代码块解决继承Thread类的方式的线程安全问题
package test;
class  SaleWindow  extends  Thread{
    
    
    private  static int ticket = 100;
    private static Object  obj=new Object();//必须加个static关键字(如果不加static关键字的,会报错,因为是继承Thread类,对象不唯一,每创建一个线程相当于new了3个对象,每个对象都有独立的一份属性)
    @Override
    public  void run() {
    
    
        while (true) {
    
    
            synchronized (obj){
    
    
//这里不能用this(慎用具体问题具体分析)充当同步监视器,因为对象不是唯一的(对象不唯一,锁也就不唯一,锁不唯一就会出现线程问题),有p2、p 2个对象。但这里可以写成SaleWindow.class用当前类去充当对象(Class clazz=SaleWindow.class(SaleWindow.class只会加载一次))
                //synchronized ()括号里面要写同步监视器,俗称:锁。任何一个类的对象都可以充当锁
                if (ticket > 0) {
    
    
                   try {
    
    
                  Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                 e.printStackTrace();
               }
                    System.out.println(Thread.currentThread().getName() + ":卖了" + ticket+"票");
                    ticket--;
                } else {
    
    
                    break;
                }
            }
        }
    }
}
package test;
public class Secure{
    
    
    public  static  void  main(String[] args) {
    
    
        SaleWindow p=new SaleWindow();
        SaleWindow p2=new SaleWindow();
        p.setName("线程1");
        p2.setName("线程2");
        p.start();
        p2.start();
    }
}

同步方法

如果操作共享的数据的代码完整的声明在一个方法中,我们可以将此方法声明同步的,一个方法使用 synchronized关键字修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法时就必须等待,直到线程A使用完该方法。

//同步方法定义格式:
public synchronized void run(){
    
    

}

使用同步方法解决实现Runnable接口线程安全问题

package test;
public class  SaleWindow implements Runnable {
    
    
    private  int  ticket=  1000 ;
    @Override
    public void run() {
    
    
        while (ticket>0&&ticket<=1000){
    
    
                show();
            }
        }
    private synchronized void  show(){
    
    //show()方法默认同步监视器:this
        if(ticket>0){
    
    
            System.out.println(Thread.currentThread().getName()+":卖了"+ticket+"票");
            ticket--;
        }
    }
}
import test.SaleWindow;

public class Secure{
    
    
    public  static  void  main(String[] args) {
    
    
        SaleWindow p =  new SaleWindow();
        Thread p1 =  new  Thread(p);
        Thread p2 =  new  Thread(p);
        p1.setName( "窗口A" );
        p2.setName( "窗口B" );
        p1.start();
        p2.start();
    }
}

使用同步方法解决实现Thread接口线程安全问题

package test;
class  SaleWindow  extends  Thread{
    
    
    private  static int ticket = 100;
    @Override
    public  void run() {
    
    
        while (true) {
    
    
            show();
        }
    }
    private static synchronized void show(){
    
    
        if (ticket > 0) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖了" + ticket+"票");
            ticket--;
        }
    }
}
package test;
public class Secure{
    
    
    public  static  void  main(String[] args) {
    
    
        SaleWindow p=new SaleWindow();
        SaleWindow p2=new SaleWindow();
        p.setName("线程1");
        p2.setName("线程2");
        p.start();
        p2.start();
    }
}
//注意:同步方法仍然涉及到同步监视器,只是不需要我们显式的声明

总结:非静态的同步方法监视器为this;静态的同步方法的同步监视器是当前类本身

锁概念

死锁指两个或两个以上的线程为了使用某个临界资源而无限制的等待下去。

例如两个人都同时到达卫生间,而且两个人都比较礼貌,第一个人和第二个人说:你先吧,第二个人和第一个人说:你先吧。这两个人就这样一直在互相礼让,谁也不进入,这种现象就是死锁。

Lock(锁)

Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当(ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在线程的安全控制下,比较常用的是ReentrantLock,可以显式加锁、释放锁)

在多线程同步机制中,若想实现在资源互斥下进行线程调度,常会用到线程锁技术。常用的方式有Lock接口和synchronized关键字。Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放同步监视器,但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到异常处理的finally{} 中(synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized不行,使用synchronized时,等待的线程会一直等待下去。通过Lock可以知道有没有成功获取锁(调用tryLock方法),而synchronized没有办法

private ReentrantLock lock=new ReentrantLock(true);
//fair公平的意思,可以设置公平线程,可以设置线程优先级
package test;
import java.util.concurrent.locks.ReentrantLock;
class  SaleWindow  implements Runnable{
    
    
    private int ticket=10;
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
    
    
        while (true){
    
    
            try {
    
    
                lock.lock();//调用锁定lock()方法
                if(ticket>0){
    
    
                    System.out.println(Thread.currentThread().getName()+" 卖了"+ticket+"票");
                    ticket--;
                }else break;
            }finally {
    
    
                //调用解锁unlock()方法
                lock.unlock();
            }
        }
    }
}
package test;
public class Secure {
    
    
    public static void main(String[] args) {
    
    
        SaleWindow p=new SaleWindow();
        Thread p1=new Thread(p);
        Thread p2=new Thread(p);
        Thread p3=new Thread(p);
        p1.setName("窗口A");
        p2.setName("窗口B");
        p3.setName("窗口C");
        p1.start();
        p2.start();
        p3.start();
    }
}
/*
输出结果:
窗口A 卖了10票
窗口A 卖了9票
窗口A 卖了8票
窗口A 卖了7票
窗口A 卖了6票
窗口A 卖了5票
窗口C 卖了4票
窗口C 卖了3票
窗口C 卖了2票
窗口C 卖了1票
*/

优先使用顺序:Lock——同步代码块——同步方法

线程的通信

wait()

让当前线程释放对象锁并进入等待(阻塞)状态

notify()

唤醒一个正在等待相应对象锁的线程(也就是被wait())如果是多个线程被wait(),则唤醒优先级高的那个。使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行

notifyAll()

唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

package test;
public class Secure {
    
    
    public static void main(String[] args) {
    
    
       SaleWindow p=new SaleWindow();
        Thread p1 = new Thread(p);
        Thread p2 = new Thread(p);
        p1.setName("线程1");
        p2.setName("线程2");
        p1.start();
        p2.start();
    }
}
class  SaleWindow  implements Runnable {
    
    
    private int number=1;
    @Override
    public void run() {
    
    
        while (true){
    
    
            synchronized (this) {
    
    
                
                notify();//2个线程使用notify()方法;而notifyAll()方法一般用于3个线程以上(也就是有2个线程被阻塞了)
                if(number<=6){
    
    
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
    
    
                        
                        wait();
//使得调用wait()方法的线程进入阻塞状态;执行wait()方法会释放锁(也就是同步监视器)
                        
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }else break;
            }
        }
    }
}
/*
输出结果:
线程1:1
线程2:2
线程1:3
线程2:4
线程1:5
线程2:6
*/

说明

wait()notify()notifyAll()三个方法必须使用在同步代码块或同步方法中

wait()notify()notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器

wait()notify()notifyAll()三个方法是定义在java.lang.Object类中;sleep()是Thread类声明的

 /*
线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
*/
class Account{
    
    
    private double balance;

    public Account(double balance) {
    
    
        this.balance = balance;
    }
    public void add(double amt){
    
    
        if(amt>0){
    
    
            balance+=amt;
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"存钱成功"+balance);
        }
    }
}

class Customer extends Thread{
    
    
    private Account acct;
    public Customer(Account acct) {
    
    
        this.acct = acct;
    }
    @Override
    public void run() {
    
    
        for(int i=1;i<=3;i++){
    
    
            acct.add(1000);
        }
    }
}
public class Multithreading {
    
    
    public static void main(String[] args) {
    
    
        Account p=new Account(0);
        Customer bank=new Customer(p);
        Customer bank1=new Customer(p);
        bank.setName("张三");
        bank1.setName("老王");
        bank.start();
        bank1.start();
    }
}

补充

线程的调度

时间片:时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复

抢占式:高优先级的线程抢占CPU

Java调度方法

同优先级线程组成先进先出队列(先到先服务)使用时间片策略

对高优先级,使用优先调度的抢占式策略(调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会)

MAX_PRIORITY:10(线程可以具有的最高优先级,取值为10)
MIN_PRIORITY:1( 线程可以具有的最低优先级,取值为1。)
NORM_PRIORITY:5(默认优先级是5

Thread类的setPriority()getPriority()方法分别用来设置和获取线程的优先级。

getPriority():获取线程优先级
setpriority(int newPriority):设置线程的优先级
package test;

import test.Multithreading;
public class Bank {
    
    
    public static void main(String[] args) {
    
    
        
        SubClass p = new SubClass();//等价于new SubClass().start();
        p.setPriority(Thread.MAX_PRIORITY);//设置线程优先级
        p.setName("线程1");//设置线程名字
        p.start();
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);//设置线程优先级
        Thread.currentThread().setName("主线程");
        for (int i = 1; i <= 20; i++) {
    
    
            if (i % 2 != 0) {
    
    
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
        }
    }
}
class SubClass extends Thread {
    
    

        @Override
        public void run() {
    
    
            for (int i = 1; i <= 20; i++) {
    
    
                if (i % 2 == 0) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
                    //Thread.currentThread().getPriority():获取线程优先级
                }
            }
        }
    }
/*
输出结果:
线程1:10:2
主线程:1:1
线程1:10:4
主线程:1:3
线程1:10:6
主线程:1:5
线程1:10:8
主线程:1:7
主线程:1:9
线程1:10:10
主线程:1:11
线程1:10:12
主线程:1:13
主线程:1:15
线程1:10:14
主线程:1:17
线程1:10:16
主线程:1:19
线程1:10:18
线程1:10:20
*/
//注意:优先级较高的并不是先执行,只是从概率上来讲较高,也就是高概率被CPU执行到(Java线程有优先级,优先级高的线程会获得较多的运行机会。)

猜你喜欢

转载自blog.csdn.net/qq_47256069/article/details/112796986