java 【多线程】

多线程

单核CPU使用的是时间片轮换来实现多线程的,多核CPU才是真正意义上的多线程

并行与并发

  • 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
  • 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

在通常执行的Main方法中,会执行三个线程,主线程Main、处理异常线程、垃圾收集器线程。

创建线程的三种的方式

  • 继承Thread类,重写run方法
  • 实现Runnable接口,实现run方法
  • 实现Callable接口,实现call方法

线程、进程

程序:程序是指令与数据的有序集合,其本身没有任何运行的含义,是一个静态的概念

进程:是程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。通常一个进程中可以包含多个线程,至少包含一个。

线程:是CPU调度和执行的单位。

真正的多线程指多个CPU,即多核。在单核的情况下,在同一时间点CPU只能执行一个代码,但是因为切换的很快,造成了同时执行的错觉。

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

继承Thread类

  1. 创建Thread子类
  2. 重写run()方法
  3. 调用Thread的start方法

要继承Thread类,具有挣强资源的能力,编写的线程任务/逻辑并不是随便写一个方法,而是要重写Thread中的run()方法。

public class TestThread extends Thread{
	@Override
	public void run(){
		for(int i = 0;i<10;i++){
			System.out.println("thread"+i);
		}
	}
}

测试与主线程挣强资源

public class Test{
    
    public static void main(String[] args){
        //主线程1
        for(int i = 0;i<10;i++){
			System.out.println("main1"+i);
		}
        
        //创建其他线程与主线程挣强资源
        TestThread thread = new TestThread();
        //thread.run(); //run方法不能直接调用
        //要启动启动线程
        thread.start();
        
        //主线程2
        for(int i = 0;i<10;i++){
			System.out.println("main2"+i);
		}
        
    }
}

你要执行的线程要先与主线程创建。
在这里插入图片描述

修改线程名称

可以使用线程父类Thread的getName()与setName()方法。

public class TestThread extends Thread{
	@Override
	public void run(){
		for(int i = 0;i<10;i++){
			System.out.println(super.getName()+i);
		}
	}
}

可以通过Thread.currentThread()获取当前线程

public class Test{
    
    public static void main(String[] args){
        //主线程1
        Thread.currentThread().setName("主线程");
        for(int i = 0;i<10;i++){
			System.out.println(Thread.currentThreaad().getName()+"1"+i);
		}
        
        //创建其他线程与主线程挣强资源
        TestThread thread = new TestThread();
        //TestThread thread = new TestThread("子线程1");
        thread.setName("子线程1"); 
        //thread.run(); //run方法不能直接调用
        //要启动启动线程
        thread.start();
        
        //主线程2
        for(int i = 0;i<10;i++){
			System.out.println(Thread.currentThreaad().getName()+"2"+i);
		}
        
    }
}

也可以使用构造器设置线程的名字。

public class TestThread extends Thread{
    public TestThread(String name){
        super(name);
    }
    
    public TestThread(){}
    
	@Override
	public void run(){
		for(int i = 0;i<10;i++){
			System.out.println(super.getName()+i);
		}
	}
}

练习-买火车票

三个窗口,每个窗口100个人强10张票。

创建买票线程

public class BuyTicketThread extends Thread{

	private static int ticketNum = 10; 
	
    public TestThread(String name){
        super(name);
    }
    
    public TestThread(){}
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
			//可以在这里制造延时
            if(ticketNum>0){
				System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
            }
		}
	}
}

因为三个线程共用一个“票”资源,所以要将ticketNum设为静态的。

public class Test{
    
    public static void main(String[] args){
        //1号窗口
        BuyTicketThread thread1 = new BuyTicketThread("1");
        thread1.start();
        //2号窗口
        BuyTicketThread thread2 = new BuyTicketThread("2");
        thread2.start();
        //3号窗口
        BuyTicketThread thread3 = new BuyTicketThread("3");
        thread3.start();
    }
}

在这里插入图片描述

运行时会出现错误

实现Runnable接口

  1. 实现Runnable接口
  2. 实现run方法
  3. 创建实现类对象,注入Thread对象中,使用Thread对象的start方法启动

不同于继承Thread类要重写run()方法,实现Runnable接口要实现run()方法。

public class TestThread implements Runnable{
    public TestThread(String name){
        super(name);
    }
    
    public TestThread(){}
    
	@Override
	public void run(){
		for(int i = 0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+"----"+i);
		}
	}
}

主线程

public class Test{
    
    public static void main(String[] args){
        //创建其他线程与主线程挣强资源
        TestThread thread = new TestThread();
        Thread tt = new Thread(thread,"子线程");
        thread.start();
        
        //主线程
        for(int i = 0;i<10;i++){//主线程名称默认为main
			System.out.println(Thread.currentThreaad().getName()+"2"+i);
		}
        
    }
}

练习-买火车票

三个窗口,每个窗口100个人强10张票。

创建买票类

public class BuyTicketThread implements Runnable{

	public int ticketNum = 10; 
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            if(ticketNum>0){
				System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
            }
		}
	}
}
public class Test{
    
    public static void main(String[] args){
        //创建线程任务
        BuyTicketThread thread = new BuyTicketThread();
        //1号窗口
        Thread t1 = new Thread(thread,"1");
        t1.start();
        //2号窗口
        Thread t2 = new Thread(thread,"2");
        t2.start();
        //3号窗口
        Thread t3 = new Thread(thread,"3");
        t3.start();
    }
}

可以看到,由于调用的是同一Runnable实现类,线程中的公用属性“票”并不用设置为静态值

问题:运行结果中还是会出现不同窗口买到同一张票的问题,这需要线程同步来解决。

龟兔赛跑

image-20220709224559049
public class Race implements Runnable{
    
    private static String winner;
    
    @Override
    public run(){
        for(int i=1;i<=100;i++){
            if("兔子".equals(Thread.currentThread.getName())){
                if(i==51){//50米睡30秒
                    sleep(15000);
                }
                run(100);//兔子要100毫秒跑一米
            }else{
                run(200);//乌龟要200毫秒跑一米
            }
        }
        if(!gameOver()){//两个都跑完,两个以上个参赛者不能这样写
            System.put.println("winner is"+Thread.currentThread.getName());
        }
    }
    
    private boolean gameOver(){
        if(winner==null){
            winner = Thread.currentThread().getName();
            return true;
        }
        return false;
    }
    
    //睡m毫秒
    private void sleep(long m){
        try{
            System.put.println(Thread.currentThread.getName()+"开始睡觉");
            Thread.sleep(m);//兔子要睡m毫秒
            System.put.println(Thread.currentThread.getName()+"睡醒了");
        }catch(Exception ex){
        	ex.printStackTrace();
        } 
    }
    
    //跑m毫秒
    private void run(long m){
        try{
            Thread.sleep(m);
            System.put.println(Thread.currentThread.getName()+"---->跑了"+i+"米");
        }catch(Exception ex){
        	ex.printStackTrace();
        } 
    }
    
    public static void main(String[] args){
        Race r = new Race();
        Thread t1 = new Thread(r,"兔子");
        Thread t2 = new Thread(r,"乌龟");
        
        t1.start();
        t2.start();
    }
    
}

在这里插入图片描述

实现Callable接口

前两种方法,无法设置返回值也无法抛出异常。Callable是一个泛型接口。

  • 实现Callable接口如果不带泛型,call()的返回值就为Object
  • 如果带泛型,那么call的返回值就是对应的泛型
  • call方法有返回值,可以抛出异常

实现方法

  1. 继承Callable接口
  2. 实现call方法
  3. 开启线程

开启线程有很多方法,我们这里给出两种,一种是用FutureTask,一种使用ExecuterService

测试随机数

public class TestRandomNum implements Callable<Integer>{
    
    @Override
    public Integer call() throw Exception{
        return new Random().nextInt(10);
    }	    
}

class Test{
    public static void main(String[] args){
        //方法一
        TestRandomNum trn1 = new TestRandomNum();
        FutureTask tf = new FutureTask(trn1);
        Thread tt = new Thread(tf);
        tt.start();
        //返回值要通过FutureTask获取
        Integer o = (Integer)tf.get();
        System.out.println("方式一"+o);
        
        System.out.println("==================================");
        
        //方法二
        TestRandomNum trn2 = new TestRandomNum();
        //创建执行服务
        ExecuterService ser = Executors.newFixedThreadPool(2);
        //提交执行
        Future<Integer> r1 = ser.submit(trn1);
        Future<Integer> r2 = ser.submit(trn2);
        //获取结果
        try{
            Integer i = r1.get();System.out.println("方式二 trn1: "+i);
        	Integer j = r1.get();System.out.println("方式二 trn2: "+j);
        }catch(Exception ex){
            ex.printStackTrace();
        }
        //关闭服务
        ser.shutdowNow();
    }
}

静态代理

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。简单来说就是:使用一个代理对象将对象包装起来,然后用该代理对象来取代该对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时调用原始对象的方法,也就是为其他对象提供一种代理以控制对这个对象的访问。

  • 真实对象与代理对象要同时实现一个接口
  • 代理对象要代理真实角色
public class StaticProxy{
    public static void main(String[] args){
        You y = new You();
        WeddingCompany w = new WeddingCompany(y); 
        w.HappyMarry();
    }
}

//结婚的接口
interface Marry{
    void HappyMarry();
}

//结婚对象 真实角色
class You implements Marry{
    
    @Override
    public void HappyMarry(){
    	System.out.println("我要结婚了");   
    }
}

//代理角色
class WeddingCompany implements Marry{
    //真是目标角色
    private Marry target;
    
    public WeddingCompany(Marry target){
        this.target = target;
    }
    
    @Override
    public void HappyMarry(){
    	before();
        this.target.HappyMarry();//真实对象的任务
        after();
    }
    
    private void after(){
        System.out.println("收尾款");
    }
    
    private void before(){
        System.out.println("布置现场");
    }
    
}

使用了静态代理模式情况下,我们没有修改真实对象类中的业务代码,而是选择以代理类的方式增强了他的功能,耦合度低,可扩展性好。静态代理由于不需要反射获取目标对象,所以性能也更好。
但代理模式会造成系统设计中类的数量增加,在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢,同时也增加了系统的复杂度。

在实现Runnable接口来实现多线程时,我们看到了实现类与Thread类都实现了Runnable接口,并且都实现了run方法,同时在开启线程时将实现类交给了Thread对象,这就是静态代理,而Thread就是静态代理类。

Lamda表达式

lambda 表达式的语法格式如下:

(parameters) -> expression
或
(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  1. 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  2. 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  3. 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  4. 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

Lamda表达式属于函数式编程。了解Lamda表达式,要先了解函数式接口==“Functional interface”==

函数式接口的定义:

  • 任何接口只要包含一个抽象方法,那么他就是函数式接口,例如Runnable接口
  • 对于函数式接口我们可以通过lamda表达式来创建
public class Test{
    
    static class Me2 implements Like{
        @Override
        public void ILike(){
            System.out.println("me2 我喜欢");
        }
    }
    
    public static void main(String[] args){
        //方式一 创建实现类
        Like like = new Me1().ILike();
        
        //方式二 静态内部类
       	like = new Me2().ILike();
        
        //方式三 局部内部类
        class Me3 implements Like{
            @Override
            public void ILike(){
                System.out.println("me3 我喜欢");
            }
        }
        like = new Me3().ILike();
        
        //方式四 匿名内部类
       	like = new Like() {
            @Override
            public void ILike(){
                System.out.println("me4 我喜欢");
            }
        };
        like.ILike();
        
        //方法五 使用Lamda表达式
        like = ()->{
            System.out.println("lamda 我喜欢");
        };
        like.ILike();
    }
}

interface Like{
    void ILike();
}

class Me1 implements Like{
    @Override
    public void ILike(){
        System.out.println("me1 我喜欢");
    }
}

避免了内部类过多,可以让代码看上去更简洁。

例子

public class Test{
    public static void main(String[] args){
        MathOperation add = (int a, int b) -> a + d;
        MathOperation sub = (a,b) -> {
            return a-b;
        };
        System.out.println("100+32 = "+add.operation(100,32));
        System.out.println("100-32 = "+sub.operation(100,32));
    }
}

interface MathOperation {
	int operation(int a, int b);
} 

使用总结:

  • Lamda表达式的使用前提必须是函数式接口
  • 表达式的()在参数只有一个时可以省略,{}在只有一行代码实现时可以省略
  • 表达式的()中参数类型可以省略

Runnable就是一个函数式接口。

生命周期

在这里插入图片描述
在这里插入图片描述

线程的常见方法

  1. start(): 启动当前线程,表面生调用start方法,实际上在调用线程中的run方法
  2. run(): 线程类,继承Thread与实现Runnable接口的时候,都要重新实现这个run方法,run中有线程中要执行的内容。
  3. currentThread(): Thread中的一个静态方法,获取当前正在执行的线程。
  4. setName(): 设置线程名
  5. getName(): 获取线程名

设置优先级

使用线程对象的setPriority与getPriority,线程优先级最小为1,最大为10,设置优先级不可以超范围,若不配置默认的优先级为5,优先级越高CPU调度的概率就越高。

若是同等级线程,实行的是先到先得。

public class TestThread01 extends Thread{
    
    @Override
    public void run(){
        for(int i=1;i<=10;i++){
            System.out.println(i);
        }
    }
}

class TestThread02 extends Thread{
    @Override
    public void run(){
        for(int i=21;i<=30;i++){
            System.out.println(i);
        }
    }
}

class Test{
    public static void main(String[] args){
        TestThread01 t1 = new TestThread01();
        //设置线程t1的优先级
        t1.setPriority(1);//优先级别高
        t1.start();
        
        TestThread02 t2 = new TestThread02();
        t2.setPriority(10);//优先级别低
        t2.start();
    }
}

join

调用join方法的线程会被优先执行,其他线程阻塞,当前线程被执行完之后,其他线程才会执行,通过线程对象调用。

public class TestThread01 extends Thread{
    
    public TestThread01(String name){
        super(name);
    }
    
    @Override
    public void run(){
        for(int i=1;i<=10;i++){
            System.out.println(this.getName()+i);
        }
    }
}
class Test{
    public static void main(String[] args){
        for(int i=1;i<=100;i++){
            if(i==12){
            	TestThread01 t = new TestThread01("子线程1");
                t.start();
                t.join();//子线程1会被优先执行
            }
            System.out.println(Threah.currentThread().getName+i);
        }
    }
}

join并不会阻塞同级,会阻塞main或者调用方线程

join()方法和sleep()方法的区别:
两者的区别在于:sleep(2000)不释放锁,join(2000)释放锁,因为join()方法内部使用的是wait(),因此会释放锁。看一下join(2000)的源码就知道了,join()其实和join(2000)一样,无非是join(0)而已:

sleep

人为的制造阻塞时间,是Thread下的一个静态方法,单位为毫秒级。

public class Test{
    public static void main(String[] args){
        try{
            Thread.sleep(3000);
       		System.out.println(Threah.currentThread().getName);
        }catch(Exception ex){
            ex.printStackTrace(); 
        }
    }
}

每个对象都有一个锁,但是sleep并不会释放锁

案例:秒表

public class Test{
    public static void main(String[] args){
       	//定义一个时间格式
       	DateFormat df = new SimpleDateFormat("HH:mm:ss");
        Date date;
        while(true){
            date = new Date();
            System.out.println(df.format(date));
            try{
                Thread.sleep(1000);
            }catch(Exception ex){
                ex.printStackTrace();
            }
        }
    }
}

在测试时,我们可以在程序的适当位置添加sleep来放大程序所存在的问题。

setDeamon

  • 线程分为用户线程与守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如:监控内存、垃圾回收、后太记录操作日志等等

设置守护线程:

将子线程设置为主线程的伴随线程,主线程结束,伴随线程也结束。要先设置在启动会出现垂死挣扎现象

public class TestThread extends Thread{
    @Override
    public void run(){
        for(int i=1;i<=1000;i++){
            System.out.println("子线程-----"+i);
        }
    }
}
class Test{
    public static void main(String[] args){
        TestThread t = new TestThread();
        t.setDeamon(true);//默认是false用户线程,会出现垂死挣扎现象
        t.start();
        
        //主线程还要输出1~10
        for(int i=1;i<=10;i++){
            System.out.println("main-----"+i);
        }
        
    }
}

main为用户线程,所以main必定要完成,而子线程为守护线程不必执行完。

yield

暂停当前正在执行的线程对象但是不阻塞(从运行转换为就绪,重新让CPU调度,但还可能是该线程继续执行),并执行其他线程,是Thread的一个静态方法

public class TestThread implements Runnable{
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"--start");
        Thread.yield();//礼让不一定成功
        System.out.println(Thread.currentThread().getName()+"--stop");
    }
}
class Test{
    public static void main(String[] args){
        TestThread tt = new TestThread();
        Thread t1 = new Thread(tt);
    	Thread t2 = new Thread(tt);
        t1.start();
        t2.start();
    }
}

getState

获得线程状态

  • 线程状态 Thread.State枚举类。线程可以处于以下状态之一:

    • NEW
      尚未启动的线程处于此状态。
    • RUNNABLE
      在Java虚拟机中执行的线程处于此状态。
    • BLOCKED
      被阻塞等待监视器锁定的线程处于此状态。
    • WAITING
      正在等待另一个线程执行特定动作的线程处于此状态。
    • TIMED_WAITING
      正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
    • TERMINATED
      已退出的线程处于此状态。

    一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。

public class Test{
    public static void main(String[] args){
        Thread t = new Thread(()->{
            for(int i=0;i<5;i++){
                try{
                    Thread.sleep(1000);
                }catch(Exception ex){
                    ex.printStackTrace();
                }
            }
            System.out.println("==============");
        });
    	
        //观察状态
        Thread.State s = t.getState();
        System.out.println(s);
        //启动后
        t.start();
        s = t.getState();
        System.out.println(s);
        
        while(s != Thread.State.TERMINATED){//
            try{
            	Thread.sleep(100);
                s = t.getState();
        		System.out.println(s);
            }catch(Exception ex){
                 ex.printStackTrace();
            }
        }
        
        //线程只能启动一次,关闭后不可再启动
        //t.start();
    }
}

stop

停止线程,通过线程对象调用。

public class Test{
    public static void main(String[] args){
      	//主线程还要输出1~10
        for(int i=1;i<=1000;i++){
            if(i==35){
                Thread.currentThread().stop();//方法已过期
            }
            System.out.println("main-----"+i);
        }
        
    }
}

这个方法已经过期了,不推荐使用,我们可以通过设置标志位来让线程正常停止

public class TestThread implements Runnable{
    private boolean stop = false;
    
    @Override
    public void run(){
        while(!stop){
        	System.out.println(Thread.currentThread().getName()+"-->"+(i++));
        }
    }
    
    public void Stop(){
        this.stop = true;
        System.out.println(Thread.currentThread().getName()+"停止了");
    }
    
    public staic void main(String[] args){
        TestThread tt = new TestThread();
        Thread t = new Thread(tt,"子线程");
        t.start();
        //线程运行一段时间,这里可以写主线程的任务
        for(int i = 0;i<=1000;i++){
            if(i==500){
                //修改标志位,停止线程
                tt.Stop();
            }
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
        
    }
    
}

线程安全问题

在买票案例时,还是会出现多张10,出现-1,0等现象,出现这种现象是因为在线程进行“减减”操作前,其他线程强占了资源。

一个线程没执行完,其他线程就进行了抢占。

我们可以一再写一个例子银行取钱,同样也有这样的错误:

public class Test{
	public static void main(String[] args){
        Account a = new Account(100,"账户");
        
        Draw d1 = new Draw(a,50,"我");
        Draw d2 = new Draw(a,100,"朋友");
        
        d1.start();
        d2.start();
    }	
}

//账户
class Account {
    public int money;
    public String name;
    
    public Account(int money, String name){
        this.money = money;
        this.name = name;
    }
}


class Draw extends Thread{
    private Account account;//账户
    private int drawMoney;//取走的钱
    
    public Draw(Account account,int drawMoney,String name){
        super(name);//线程名
        thia.account = account;
        this.drawMoney = drawMoney;
    }
    
    @Override
    public void run(){
        if(account.money - drawMoney<0){
            System.out.println(this.getName()+"钱不够");
        }
        
        //给予于时间暴露问题
        try{
            Thread.sleep(1000);
        }catch(Exception ex){
            ex.printStrackTrace();
        }
        
        account.money = account.money - drawMoney;
        System.out.println(this.getName()+"取走:"+drawMoney);
        System.out.println("余额:"+account.money);
    }
    
}

运行此程序,发现余额为-50。

解决:加入锁、同步代码块

锁的引入

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized。

当一个线程获得对象的排它锁(又称为写锁((eXclusive lock,简记为X锁)),若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。),独占资源,其他线程必须等待,使用后释放锁即可,存在下述问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;

  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;

  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候需要线程同步,线程同步是一个等待机制,多个需要同时访问此对象的线程进入这个 对象的等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。

同步代码块

加工买票类,将具有安全隐患的代码包裹起来。

public class BuyTicketThread implements Runnable{

	public int ticketNum = 10; 
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            synchronized(this){
                if(ticketNum>0){
					System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
            	}
            }
		}
	}
}

以上代码修改的是实现Runable接口,this指代的是当前对象,由this来充当这把锁

继承Thread类也可以做同样改动,同样也由this充当锁。

public class BuyTicketThread extends Thread{

	private static int ticketNum = 10; 
	
    public TestThread(String name){
        super(name);
    }
    
    public TestThread(){}
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            synchronized(this){
                if(ticketNum>0){
                    System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
                }
            }
		}
	}
}

运行main类
在这里插入图片描述

如果锁住的话,这个票应该是按顺序的,但结果并不是,所以this这把锁并没有锁住。

为什么哪?是因为this指代对象不同

  • 实现Runable中,this指代同一对象。开启线程时我们调用的是同一Runable实现类对象。
  • 继承Thread类,this指代不同对象。开启线程时我们调用的是不同的Thread子类对象。这就像上厕所,但每个人认为厕所有人的标准都不同,他认为红色是有人,他认为绿色是有人。

所以在继承Thread类中,锁设置一个常量"zzb"或者类的字节码信息"BuyTicketThread.class"即可

	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            synchronized(BuyTicketThread.class){
                if(ticketNum>0){
                    System.out.println("从第"+super.getName()+"窗口买到了第"+(ticketNum--)+"张票");
                }
            }
		}
	}

注意

https://www.bilibili.com/video/BV1aw411o7rr?p=299&t=720.4

同步监视器总结:

synchronized(同步监视器){}

  1. 必须是引用数据类型,不能是基本数据类型
  2. 可以创建一个专门的同步监视器,没有任何业务意义
  3. 一般使用共享资源作为同步监视器
  4. 在同步代码块中不能改变同步监视器对象的引用
  5. 尽量不使用String和包装类作为同步监视器
  6. 建议使用final修饰同步监视器

同步代码块执行过程:

  1. 第一个线程来到同步代码块,发现同步监视器open状态,需要close关闭代码块,然后执行其中的代码
  2. 第一个线程执行过程中,发生了线程切换(阻塞就绪),第一个线程失去了cpu,但是没有开锁open
  3. 第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
  4. 第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
  5. 第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)

强调:同步代码块中能发生CPU的切换吗?能!!!但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)

多个代码块使用同一同步监视器(锁),锁住一个代码块的同时,也会锁住使用该同步监视器的其他代码块,其他线程无法访问其中的任何一个代码块

多个代码块使用同一同步监视器(锁),锁住一个代码块的同时,也会锁住使用该同步监视器的其他代码块,但是没有锁住使用其他同步监视器的代码块,这些代码块其他线程仍可访问
在这里插入图片描述

同步方法

加工买票类,将具有安全隐患的代码包裹起来

public class BuyTicketThread implements Runnable{

	public int ticketNum = 10; 
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            buy();
		}
	}
    
    public synchronized void buy(){
        if(ticketNum>0){
			System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
        }
    }
    
}

继承Thread类也可以做同样改动,但是要将方法设为静态的,这样不同的对象就会调用同一方法了。

public class BuyTicketThread extends Thread{

	private static int ticketNum = 10; 
	
    public TestThread(String name){
        super(name);
    }
    
    public TestThread(){}
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            buy();
		}
	}
    
    public static synchronized void buy(){//设为静态
        if(ticketNum>0){
			System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
        }
    }
}

多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。锁一般都是引用数据类型的。

关于同步方法:

  • 不要将run()定义为同步方法
  • 非静态同步方法的同步监视器是this
    静态同步方法的同步监视器是类名.class字节码信息对象
  • 同步代码块的效率要高于同步方法了
    原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
  • 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用真他监视器的代码块

Lock

Lock是一个接口

public class BuyTicketThread implements Runnable{

	public int ticketNum = 10; 
    
    Lock lock = new ReenttrantLock();
    
	@Override
	public void run(){
		//每个窗口都有100个人在买票
		for(int i = 0;i<100;i++){
            lock.lock();//打开锁
            try{
                buy();
            }catch(Exception ex){
                ex.printStackTrace();
            }finally{
                lock.unlock();//关闭锁
            }
		}
	}
    
    public void buy(){
        if(ticketNum>0){
			System.out.println("从第"+Thread.currentThread.getName()+"窗口买到了第"+(ticketNum--)+"张票");
        }
    }
    
}

Lock与synchronized的区别:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

选取的优先顺序:

Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

线程同步中的优缺点

对比:
线程安全,效率低
线程不安全,效率高I

可能造成死锁:

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

死锁问题解决减少同步资源的定义,避免嵌套同步

线程通信

在这里插入图片描述

阶段一

创建产品类,包括品牌名,与商品名

public class Product{
    private String brand;
    private String name;
    
    public String getBrand(){
        return this.brand;
    } 
    
    public void setBrand(String brand){
        this.brand = brand;
    }
    
    public String getName(){
        return this.brand;
    } 
    
    public void setName(String name){
        this.name = name;
    }
    
}

创建生产者进程,创建十个产品,在适当位置添加sleep时间,使问题在运行时暴露出来

public class ProducerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            if(i%2 == 0){
            	p.setBrand("哈尔滨");
                try{
                    Thread.sleep(1000);
                }catch(Exception ex){
                    ex.printStackTrace();
                }
                p.setName("啤酒");
            }else{
                p.setBrand("德芙");
                try{
                    Thread.sleep(1000);
                }catch(Exception ex){
                    ex.printStackTrace();
                }
                p.setName("巧克力");
            }
            System.out.println("生产者生产了:"+p.getBrand()+"---"+p.getName());
        }
    }

}

创建消费者进程

public class ConsumerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("消费这消费了:"+p.getBrand()+"---"+p.getName());
        }
    }

}

测试类

public class Test{
    public static void main(String[] args){
      	Product p = new Product();
        ProducerThread pt = new ProducerThread(p);
        ConsumerThread ct = new ConsumerThread(p);
        
        pt.start();
        ct.start();
    }
}

在这里插入图片描述
如此执行,产生了两个问题:

  1. 生产者与消费者没有交替执行
  2. 打印错误,哈尔滨—null,这是没有同步产生的问题,生产者生产到中途,被消费者抢占了。

阶段二

解决同步问题(问题2)。

同步代码块

修改生产者,修改时同步监视器的选取很重要,当生产者执行时,消费者不能进行执行,反之亦然。所以要同时锁住,this肯定不行,因为我们这里是继承了Thread类,线程开启是使用到了两个完全不同的对象。在同步监视器的选取中提到,可以选取共享资源,所以这里使用产品p作为同步监视器。

在被锁住线程运行时,若被锁住线程需要处CPU外其他资源进入阻塞状态,那么其他线程仍可以抢占(CPU),但是同锁线程不能运行。

public class ProducerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            synchronized(p){
                if(i%2 == 0){
                    p.setBrand("哈尔滨");
                    try{
                        Thread.sleep(1000);
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }
                    p.setName("啤酒");
                }else{
                    p.setBrand("德芙");
                    try{
                        Thread.sleep(1000);
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }
                    p.setName("巧克力");
                }
                System.out.println("生产者生产了:"+p.getBrand()+"---"+p.getName());
            }
        }
    }

}

消费者

public class ConsumerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            synchronized(p){
                System.out.println("消费这消费了:"+p.getBrand()+"---"+p.getName());
            }
        }
    }

}
image-20220708205248718

运行发现,打印错误问题得到解决,顺序仍有错误。

同步方法

也要重视锁的问题,若将方法提出来,改成同步方法,这样调用时生产者与消费者线程的同步监视器还是不同的,还是锁不住。所以还是要抓住共享的产品类,在产品类中添加同步方法。

public class Product{
    private String brand;
    private String name;
    
    public String getBrand(){
        return this.brand;
    } 
    
    public void setBrand(String brand){
        this.brand = brand;
    }
    
    public String getName(){
        return this.brand;
    } 
    
    public void setName(String name){
        this.name = name;
    }
    
    //生产商品
    public synchronized void SetProduct(String brand, String name){
        this.setBrand(brand);
        try{
        	Thread.sleep(1000);
        }catch(Exception ex){
        	ex.printStackTrace();
        }
        this.setName(name);
        System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());
    }
    
    //消费商品
    public synchronized void GetProduct(){
        System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());
    }
    
}
public class ProducerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            if(i%2 == 0){
                p.SetProduct("哈尔滨","啤酒");
            }else{
                p.SetProduct("德芙","巧克力");
            }
        }
    }

}
public class ConsumerThread extends Thread{
	
   	private Product p;
    
    public ProducerThread(Product p){
        this.p = p;
    }
    
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            p.GetProduct();
        }
    }

}

阶段三

解决交替问题,要生产之后在消费。
在这里插入图片描述

在同步方法上进行修改

public class Product{
    private String brand;
    private String name;
    private boolean flag = false;
    
    public String getBrand(){
        return this.brand;
    } 
    
    public void setBrand(String brand){
        this.brand = brand;
    }
    
    public String getName(){
        return this.brand;
    } 
    
    public void setName(String name){
        this.name = name;
    }
    
    //生产商品
    public synchronized void SetProduct(String brand, String name){
        if(flag){//有商品,等待消费
            try{
                wait();
            }catch(Excption ex){
                ex.printStackTrace();
            }
        }
        
        this.setBrand(brand);
        try{
        	Thread.sleep(1000);
        }catch(Exception ex){
        	ex.printStackTrace();
        }
        this.setName(name);
        System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());
        
        //生产完
        flag = true;//有商品
        //通知消费者来消费
        notify();
    }
    
    //消费商品
    public synchronized void GetProduct(){
        if(!flag){//有商品,等待生产
            try{
                wait();
            }catch(Excption ex){
                ex.printStackTrace();
            }
        }
        
        System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());
        
        //消费完
        flag = false;//没有商品
        //通知生产者来生产
        notify();
        
    }
    
}
image-20220708213250574

在Java对象中,有两种池

锁 池:synchronized
等待池:wait(),notify(),notifyAll()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),如果未来的某一时刻,另外一个线程调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤起,然后进入到对象的锁池里面去获得该对象的锁,如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait方法之后

等待池的方法wait,notify,notifyAll必须要在同步代码块或者同步方法中才可以,否则会报错。

sleep不释放锁,wait释放锁。notify唤醒一个,notifyAll唤醒全部。

在这里插入图片描述

Lock下的线程通信

以上情况只有一个生产者一个消费者,并且都在一个等待池中,那么唤醒的就不一定是谁了。所以可以是生产者与消费者在不同的等待池中。

Condition是在lava1.5中才出现的,它用来替代传统的Object的wait、notify实现线程间的协作,相比使用Object的wait、notify,使用Condition的await、signal这种方式实现线程间协作更加安全和高效。
它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列。
Object中的wait,notify,notifyAll方法是和“同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与“互斥锁"/”共享锁“捆绑使用的。
调用Condition的await、signal、signalAll方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock()之间才可以使用

  • Conditon中的await对应Object的wait;
  • Condition中的signal对应Object的notify;
  • Condition中的signalAll对应Object的notifyAll;

同一个锁lock,可以通过newCondition来创建等待池。

public class Product{
    private String brand;
    private String name;
    private boolean flag = false;//false没有商品
    //声明一个锁
    private Lock lock = new ReentranLock();
    
    Condition producerCondition = lock.newCondition();
    Condition consumerCondition = lock.newCondition();
    
    public String getBrand(){
        return this.brand;
    } 
    
    public void setBrand(String brand){
        this.brand = brand;
    }
    
    public String getName(){
        return this.brand;
    } 
    
    public void setName(String name){
        this.name = name;
    }
    
    //生产商品
    public void SetProduct(String brand, String name){
        lock.lock();
        try{
            if(flag){//有商品,等待消费
                try{
                    producerCondition.await();//进入等待池,并释放锁
                }catch(Excption ex){
                    ex.printStackTrace();
                }
            }

            this.setBrand(brand);
            try{
                Thread.sleep(1000);
            }catch(Exception ex){
                ex.printStackTrace();
            }
            this.setName(name);
            System.out.println("生产者生产了:"+this.getBrand()+"---"+this.getName());

            //生产玩商品
            flag = true;
            //唤醒消费池中线程
            consumerCondition.signal();
            
        }catch(Exception ex){
            ex.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    //消费商品
    public void GetProduct(){
        lock.lock();
        try{
            if(!flag){//有商品,等待生产
                try{
                    consumerCondition.await();
                }catch(Excption ex){
                    ex.printStackTrace();
                }
            }

            System.out.println("消费这消费了:"+this.getBrand()+"---"+this.getName());

            //消费完
            flag = false;//没有商品
            //唤醒生产者来生产
            producerCondition.signal();
        }catch(Exception ex){
            ex.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值