Java多线程

并行与并发

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行;所以无论从微观还是从宏观来看,二者都是一起执行的;
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行

线程

我们的之前的代码执行路径只有一条,就是main(),顺序执行代码,这种环境称之为单线程环境;但假如程序途中某个环节比较耗时(比如进行大的文件的操作),在他后面的程序必须等待这个耗时的程序执行完成之后再去执行,这样用户体验很不好;

解决方案:主线与支线齐头并进
在这里插入图片描述
进程和线程之间的关系:

比如QQ音乐运行了,这时候这个应用程序属于一个进程,QQ音乐里面可能需要执行很多不同的任务,比如歌曲下载、歌曲的播放……,这些任务都属于线程;

现在的计算机,Windows电脑支持多进程,可以同时多个进程运行;我在电脑上开启了WPS和QQ音乐,我感觉我一边在编辑文字,一边在听歌,是同时进行的,那么 计算机在同一个时间点上,是同时进行多个进程的吗?

当然不是,因为计算机在同一时间点上只能运行一个进程,你的感觉是多个进程在同时执行,这是因为CPU在多个进程之间高速的切换,我们指的是 单核CPU(用来处理任务的核心数量),你的人耳和眼睛根本感觉不到;当然,4核或者多核CPU可以实现真正意义上的同时运行多个进程;

多进程存在的意义不是提高了执行速度,而是 提高了CPU的使用率
我们程序在运行时的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大;也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了CPU的使用率;
因为CPU本身作为硬件,一直在不断的提高自己的性能,因此我们支持多线程环境,以便于CPU的资源不被浪费;
但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以 多线程具有随机性;

Java程序运行原理:

Java命令会启动JVM,等于启动了一个应用程序,也就是启动了一个进程;
该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法,
所以 main方法运行在主线程中;

JVM的启动是多线程的吗:

JVM的启动至少启动了垃圾回收线程和主线程,所以是多线程的;进程是由系统创建,Java本身不能直接调用系统功能,也就没有办法实现多线程,但是Java给定一个类Thread来描述线程,它的底层与C/C++代码进行交互,由这些C/C++代码再与底层系统打交道,这个类辅助我们创建线程、开启线程;

线程的创建方式

线程的命名

public final String getName()

返回该线程的名称;

public final void setName(String name)

改变线程名称,使之与参数 name 相同;

public Thread(String name)

线程的有参构造需要一个线程名字,可以直接给线程设置名字;当然,自定义线程类可以添加一个有参构造,将线程名字传递给父类构造super()
 System.out.println("此处为主线程的执行");
    //创建2个子线程
    MyThread myThread1 = new MyThread();
    MyThread myThread2 = new MyThread();

    //设置2个子线程的名称
    myThread1.setName("新的线程名称1");
    myThread2.setName("新的线程名称2");

    //开启2个子线程,多个线程并行交替执行,看谁先抢到CPU执行权就先执行那个子线程
    //start方法,去调用该线程的run方法;同一个线程不可以重复开启,否则报错;
    myThread1.start();
    myThread2.start();

    //主线程继续向下走,与子线程并发执行,齐头并进。
    System.out.println("主线程的后续代码与子线程并发执行");

创建线程方式一

自定义类继承Thread,重写该类里面的run();一般来说,我们把一些耗时的代码写在run(),而不会写到主线程中,阻塞主线程;
run()里面本质上就是需要并发执行的任务;

package demo2;

public class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //通过获取当前线程对象来获取线程名字
            System.out.println(Thread.currentThread().getName()+"子线程的执行");
            //通过调用对象.getname()来获取线程名字
            System.out.println(this.getName()+"子线程的执行");
        }
    }
    //通过有参构造设置线程名字
    public MyThread1(String name) {
        super(name);
    }

    public MyThread1() {
        super();
    }
}

MyThread1 scq = new MyThread1("scq");
scq.start();
  • new对象,使对象调用start()方法,是线程开启的正确方法;
  • 当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。- - 只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法;
  • start()来启动线程,无需等待run()里面的代码执行完毕,可以直接继续执行下面的代码;这里把run()叫做线程体,它包含了要执行的这个线程的内容,run()运行结束,此线程终止,然后CPU再调度其他线程;

线程创建方法二

实现Runnable接口,这种方式扩展性强,实现一个接口 还可以再去继承其他类 ;
a:如何获取线程名称
b:如何给线程设置名称
c:实现接口方式的好处
可以避免由于Java单继承带来的局限性;

Runnable本质上就是一个任务,它是一个接口,里面只有一个run();
注意:这个类里面不能调用this.getName()来获取线程名字,因为他本质不是一个Thread;

package demo9;

class MyRunnable implements Runnable{


    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName()+"子线程");
        }
    }
}

package demo9;

public class test1 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable,"scqq");
        thread.start();

    }
}


创建线程方式3

实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果,( FutureTask 是 Future 接口的实现类);
实现步骤:

1.创建一个类实现Callable 接口
2.创建一个FutureTask类将Callable接口的子类对象作为参数传进去
3.创建Thread类,将FutureTask对象作为参数传进去
4.开启线程

package demo10;

import java.util.concurrent.Callable;
//返回值为Intreger类型

//实现累加线程任务 主线程获取累加的结果
class MyCallable implements Callable<Integer> {
private int num;

    public MyCallable(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
       int sum=0;
        for (int i = 1; i <= num; i++) {
            sum+=i;
        }
        return num;
    }
}

package demo10;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//创建线程的方式三: 线程执行结束后可以获取线程执行结束的结果在主线程使用, 有返回值,
public class test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallable myCallable = new MyCallable(100);
        FutureTask<Integer> integerFutureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(integerFutureTask);
        thread.start();
        //获取异步执行完的结果
        Integer integer = integerFutureTask.get();
        System.out.println(integer);



    }
}

runnable与callable的区别
runnable重写run方法 没有返回值 不允许抛出异常
callable 重写call方法 可以获取异步执行完的结果,call方法可以抛出异常

多线程复制文件

需求:同一个种类的3个不同线程复制一个文件;

方法一:读取一M开一个线程去复制,并追加到文件中。

RandomAccessFile in=new RandomAccessFile("C:\\Users\\Administrator\\Desktop\\dd.mp3", "rw");
int len=0;

byte[] bytes = new byte[1024*1024];
while ((len=in.read(bytes))!=-1){
    MyThread myThread = new MyThread(len, bytes);
    myThread.start();
}
in.close();
System.out.println("ok");
package demo1;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;


public class MyThread extends Thread {
    private Integer len;
    private byte[] bytes;
    public MyThread(Integer len ,byte[] bytes) {
        this.len=len;
        this.bytes=bytes;
    }
    @Override
    public void run() {
        FileOutputStream out=null;
        try {
            out = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\newcopy.mp3",true);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        try {
            out.write(bytes,0,len);
            System.out.println("写入文件");
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

方法二:使用指针来控制复制的位置

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;


public class test {
    public static void main(String[] args) throws FileNotFoundException {
        //1.封装文件
        File file = new File("C:\\Users\\Administrator\\Desktop\\dd.mp3");
        //2.获取文件大小
        long Alllength = file.length();
        //3. 定义线程个数
        int ThreadNum=3;
        //3. 获取每一个线程需要复制的文件大小
       long AverageLength=Alllength/ThreadNum;
        //4.确定每一个线程的复制开始位置与复制结束位置
        for (int i = 0; i < ThreadNum; i++) {
            long start=i*AverageLength;
            long end=AverageLength*(i+1);
           new CopyThread(start,end,file,new File("C:\\Users\\Administrator\\Desktop\\newdd.mp3")).start();
        }
        //5.如果文件大小不是开启线程个数的整数倍,就表示有多余字节没有线程读取,因此开启新的线程读取剩余字节。
        long yu=Alllength%ThreadNum;
        if (yu!=0){
            //指针复制开始位置
            long start=AverageLength*ThreadNum;
            //指针复制结束位置为文件的长度
            long end=Alllength;
            new CopyThread(start,end,file,new File("C:\\Users\\Administrator\\Desktop\\newdd.mp3")).start();
        }



    }


}
class CopyThread extends Thread{
    private long start;
    private long end;
    RandomAccessFile in=null;
    RandomAccessFile out=null;

    public CopyThread(long start, long end, File scrfile, File newfile) throws FileNotFoundException {
        this.start = start;
        this.end = end;
        in=new RandomAccessFile(scrfile, "rw");
        out=new RandomAccessFile(newfile, "rw");
    }

    @Override
    public void run() {

        try {
            //从指定位置开始读
            in.seek(start);
            //从指定位置开始写
            out.seek(start);
            byte[] bytes=new byte[1024*8];
            int len=0;
            int count=0;
            //复制文件的范围大小在end-start,超过就不再读取,避免重复复制而覆盖文件
            while (count<=end-start&&(len=in.read(bytes))!=-1){
                out.write(bytes,0,len);
                count+=len;
            }

            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }



}

Java中如何调度线程

分时调度:UNIX;

抢占式调度:Windows、Java;

在上面的程序执行当中,有这样一个现象,无论子线程如何争抢CPU资源,随机调度,主线程的代码总会在子线程之前执行,这是为什么?

  • 其实这是由于线程的优先级;
  • 一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同,不过主线程先启动占用了cpu资源,这就造成了主线程不会被阻塞;
    如果存在主线程和子线程争抢cpu执行权的话,看运气,谁抢到就让谁执行;

如何设置线程的优先级?

public final int getPriority()

返回线程的优先级;

public final void setPriority(int newPriority)

更改线程的优先级;


线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5;

  • 其实设置了优先级,也无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行;
  • 因为线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了,但是我们都知道多线程具有随机性,所以有的时候一两次的运行说明不了问题;
  MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        MyThread th3 = new MyThread();

        //设置线程的优先级 范围1--10
        th1.setPriority(Thread.MIN_PRIORITY);
        th3.setPriority(Thread.MAX_PRIORITY);

        //多个线程并发执行()多个抢占CPU的执行权,哪个线程抢到,在某一个时刻,就会执行哪个线程。线程的执行具有随机性。
        //获取线程的优先级
        int priority1 = th1.getPriority();
        int priority2 = th2.getPriority();
        int priority3 = th3.getPriority();
        //线程默认的优先级是5
        //Thread.MAX_PRIORITY;
        //Thread.MIN_PRIORITY;
        //Thread.NORM_PRIORITY

多线程下的线程控制

休眠线程

让当前正在运行的线程休眠,睡一会;


public static void sleep(long millis) throws InterruptedException

在

指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响,该线程不丢失任何监视器的所属权。sleep()平台移植性
好。

package demo3;

 class MyThread2 extends Thread{
   // sleep方法是让当前线程休眠,例如做安卓页面需要插入广告,插入广告时就可以让主线程休眠
   // 父类没有抛出异常,子类在重写时也不能跑出异常

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 10000; i++) {
            System.out.println(this.getName()+"子线程");
        }
    }
}

package demo3;

public class test1 {
    public static void main(String[] args) {
         /*  System.out.println("主进程");
        Thread.currentThread().sleep(1000);
        System.out.println("主进程停顿1000ms后执行的语句。。");*/
        MyThread2 myThread2 = new MyThread2();
        myThread2.start();
    }
}

阻塞线程

加入线程: public final void join()

意思就是: 等待该线程执行完毕了以后,其他线程才能再次执行;

注意事项: 在线程启动之后,再调用方法;

很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了;

package demo4;

 class MyThread4 extends Thread{
     public MyThread4(String name) {
         super(name);
     }

     // join方法:让多个子线程从并行转为串行,不去抢CPU执行权
    // 一个线程执行完才能执行下一个线程
    @Override
    public void run() {

        for (int i = 0; i < 10000; i++) {
            System.out.println(this.getName()+"子线程");
        }
    }
}

package demo4;

public class test1 {
    public static void main(String[] args) throws InterruptedException {
       // join方法:让多个子线程从并行转为串行,不去抢CPU执行权 ,一个线程执行完才能执行下一个线程
        MyThread4 one = new MyThread4("子线程1");
        MyThread4 two = new MyThread4("子线程2");
        MyThread4 three = new MyThread4("子线程3");
       one.start();
       one.join();
       two.start();
       two.join();
       three.start();
       three.join();


    }
}

礼让线程

礼让线程: public static void yield():

暂停当前正在执行的线程对象,并执行其他线程;
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

package demo5;

 class MyThread5 extends Thread{
     public MyThread5(String name) {
         super(name);
     }

    @Override
    public void run() {
         //线程礼让
        Thread.yield();
        for (int i = 0; i < 10000; i++) {
            System.out.println(this.getName()+"子线程");
        }
    }
}

package demo5;

public class test1 {
    public static void main(String[] args) throws InterruptedException {
       // 线程的礼让: yield方法 ,一个线程执行一次,交替
        MyThread5 scq1 = new MyThread5("scq1");
        MyThread5 scq2 = new MyThread5("scq2");
        scq1.start();
        scq2.start();

    }
}

这个礼让是要暂停当前正在执行的线程,放弃了当前抢到的CPU时间片,这个暂停的时间是相当短的,如果在这个线程暂停完毕以后,其他的线程还没有抢占到CPU的执行权,那么这个时候这个线程应该再次和其他线程抢占CPU的执行权;因此不是说A礼让了就一定要执行B;

sleep()和yield()的区别
①:sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

②:sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程;

③:另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I/O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行;

守护线程

守护线程: public final void setDaemon(boolean on):

true代表为守护线程,默认为false;
将该线程标记为守护线程或用户线程,当正在运行的线程都是守护线程时,Java 虚拟机退出;
该方法必须在启动线程前调用;
一般子线程是守护线程,主线程死亡,守护线程立马挂掉;


        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        th1.setName("张飞");
        th2.setName("关羽");
        //设置为守护线程 当主线程死亡后,守护线程要立马死亡掉。
        //注意:setDaemon(true)该方法必须在启动线程前调用。
        th1.setDaemon(true);
        th2.setDaemon(true);
        th1.start();
        th2.start();

        Thread.currentThread().setName("刘备:主线程");
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "==" + i);
        }

        System.out.println(Thread.currentThread().getName() + "退出了");
        //        刘备:主线程==0
//        刘备:主线程==1
//        张飞0
//        刘备:主线程退出了
//        张飞1
//        张飞2 
//        张飞3
//        张飞4
    }
}
---自定义线程类:
public class MyThread extends Thread {
@Override
public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }

用户线程和守护线程都是线程,区别是Java虚拟机在所有用户线程dead后,程序就会结束。而不管是否还有守护线程还在运行,若守护线程还在运行,则会马上结束。很好理解,守护线程是用来辅助用户线程的,如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。

用户线程和守护线程的适用场景

由两者的区别及dead时间点可知,守护线程不适合用于输入输出或计算等操作,因为用户线程执行完毕,程序就dead了,适用于辅助用户线程的场景,如JVM的垃圾回收,内存管理都是守护线程,还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。

创建守护线程

调用线程对象的方法setDaemon(true),设置线程为守护线程。
1、thread.setDaemon(true)必须在thread.start()之前设置;
2、在Daemon线程中产生的新线程也是Daemon的;
3、不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑;因为Daemon Thread还没来得及进行操作,虚拟机可能已经退出了。

Java守护线程和Linux守护进程

两者不是一个概念,Linux守护进程是后台服务进程,没有控制台。
在Windows中,你可以运行javaw来达到释放控制台的目的,在Unix下你加&在命令的最后就行了,所以守护进程并非一定需要的;

中断线程

public final void stop():

停止线程的运行

public void interrupt():


中断线程,查看API可得当线程调用wait()、sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞;

interrupt():不要以为它是中断某个线程,它只是给线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的;

package demo7;

public class test1 {
    public static void main(String[] args) throws InterruptedException {
        //清除阻塞状态 interrupt()方法
        // sleep: 让子线程睡3s再执行,就处于阻塞状态
        System.out.println("主线程开始");
        MyThread5 scq1 = new MyThread5("scq1");
        scq1.start();
        scq1.interrupt();//此代码让休眠的子线程不再休眠直接执行
        System.out.println("主线程结束");


    }
}

package demo7;

 class MyThread5 extends Thread{
     public MyThread5(String name) {
         super(name);
     }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 10000; i++) {
            System.out.println(this.getName()+"子线程");
        }
    }
}

小练习:复制2个文件,先复制大文件,再复制小文件

//让复制大小文件的代码放在线程中做 执行并发 就可以在大文件正在复制的同时,小文件先行完成复制才做;
//如果不采用线程复制文件,只有等大文件复制完成才可以复制小文件,体验感不好。

package demo8;

public class test1 {
    public static void main(String[] args) throws InterruptedException {
        
        System.out.println("开始复制文件");
        //开启守护线程 一旦主线程执行结束,子线程也不再执行
        new MyThread2("ss1").start();
        new MyThread6("ss2").start();


        System.out.println("文件复制完毕");


    }
}

package demo8;

import javafx.scene.shape.Path;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

class MyThread6 extends Thread{
     public MyThread6(String name) {
         super(name);
     }

    @Override
    public void run() {
        try {
            Files.copy(Paths.get("test1.java"), Paths.get("C:\\Users\\Administrator\\Desktop\\copy.java"), StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

package demo8;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

class MyThread2 extends Thread{
     public MyThread2(String name) {
         super(name);
     }

    @Override
    public void run() {
        try {
            Files.copy(Paths.get("C:\\Users\\Administrator\\Desktop\\dd.mp3"), Paths.get("C:\\Users\\Administrator\\Desktop\\copy.mp3"), StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

线程不安全

  • 使用多线程模拟车站卖票,车站的票数一开始都是确定好的,多个窗口使用多个线程并发执行来模拟;
  • 当我们模拟了一下真实售票,网络延迟Thread.sleep(50);的一个情况,会出现一些不合理的数据,也就是说出现了线程安全方面的问题。

方法一: extends Thread并将票数静态化

 /* 需求:某电影院目前正在上映贺岁大片,
共有100张票,而它有3个售票窗口售票,
请设计一个程序模拟该电影院售票。
 通过继承Thread类实现*/


 public static void main(String[] args) {
         CellRunnable th = new CellRunnable();
         CellRunnable th1 = new CellRunnable();
         CellRunnable th2 = new CellRunnable();
         th1.setName("窗口1");
         th2.setName("窗口2");
         th.setName("窗口3");
         th1.start();
         th2.start();
         th.start();
package demo11;

public class CellRunnable extends Thread {
    //共享数据,被多个线程所共享。变量共享 一个对象改变这个共享变量值,另一个变量继续使用改变后的值,不会重置。
    //每一个窗口共享总票数,而不是每一个窗口都有100张票
    static int piao = 100;
    @Override
    public void run() {
       while(true){
         try {
               Thread.sleep(50);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (piao>0) System.out.println(this.getName()+"正在出售:"+(piao--)+" 张票");
       }
    }
}

实验结果:
在这里插入图片描述

方法二:自定义类实现Runnable接口,创建一个任务开启三个线程去做 因此也是共享变量int piao=100 不需要加static

package demo12;

public  class test {
        /* 需求:某电影院目前正在上映贺岁大片,
       共有100张票,而它有3个售票窗口售票,
       请设计一个程序模拟该电影院售票。
        通过继承Thread类实现*/


        public static void main(String[] args) {
                //创建一个任务开启三个线程去做 因此也是共享变量int piao=100 不需要加static
                CellRunnable cellRunnable = new CellRunnable();
                Thread th = new Thread(cellRunnable);
                Thread th1 = new Thread(cellRunnable);
                Thread th2 = new Thread(cellRunnable);
                th1.setName("窗口1");
                th2.setName("窗口2");
                th.setName("窗口3");
                th1.start();
                th2.start();
                th.start();


        }
}

package demo12;

public class CellRunnable implements Runnable {
    //共享数据,被多个线程所共享。变量共享 一个对象改变这个共享变量值,另一个变量继续使用改变后的值,不会重置。
    //创建一个任务开启三个线程去做 因此也是共享变量int piao=100 不需要加static
    int piao = 100;
    @Override
    public void run() {
       while(true){
         try {
               Thread.sleep(50);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (piao>0) System.out.println(Thread.currentThread().getName()+"正在出售:"+(piao--)+" 张票");
       }
    }
}

实验结果:
在这里插入图片描述

为什么会出现0票或者负数票?

  • 出现负数票的问题,由于线程的随机性所导致的。 多个线程都在并发抢,网络延迟,让其他线程抢到之后都会进入if语句中,休眠期过了就会减票数,导致负票;

1、假设当th1线程这时候获得了CPU的执行权,这个时候假设票数=1了,因为我们在run()里面睡眠了50ms,在这50ms内,很有可能其他线程获得了CPU的执行权,

2、假设这时候th2获得了执行权,他也进入了while循环,判断此时的票数还是1,因此进入if循环,

3、假设这时候th1睡醒了,th2进入睡眠,th1执行了卖票语句,这时候票数变为0,

4、th2线程睡醒之后,也执行了卖票语句,这时候票数就变成了负数,或者同样情况下可能出现的0票;
在这里插入图片描述
为什么会出现相同票数?

  • ++ – 多个线程读取数据到自己工作内存,修改后再写入主存,线程A更新2的时候 线程B也在读取主存,因此可能会打印重复值,
  • ++ – i++ i-- 不是一个原子性的操作。经过读 改 写。 因此线程更新速度不同,会覆盖主存的值

由于线程的原子性造成的;原子性指的是不可分割性,线程中对于卖票的操作不是原子性操作,首先,我们需要将三个线程都共享的数据票数从主存中读取到每个线程的工作内存,然后在每个线程的工作内存中进行修改,修改完再把这个数据写回到主存当中;

1、在这个过程当中,假设线程th1先获得CPU执行权,他读取到了票数4,

2、然后在自己的工作内存当中修改了票数为3,还没来得及刷回主存当中,

3、线程th2就抢走了CPU执行权,读取到了票数4,

4、它也改了票数为3,

5、这时候th1又抢回了CPU执行权,将票数3刷回主存,因此票数显示3;

6、这个时候th2抢走了cpu执行权,票数也显示了3;

最终造成显示相同票数的原因;

在这里插入图片描述
经过上面的卖票案例,我们可以看到,多线程环境虽然提高了效率,提高了用户体验,但是出现了数据安全的问题,出现数据安全的问题主要是由于以下几点原因:

1、是不是一个多线程环境;

2、多个线程有没有共享数据;

3、有没有多条语句在操作共享变量; i++

同步代码块

前两个条件在我们的卖票案例当中是不可缺少的,要想解决数据安全的问题,只能想办法打破第三个问题;我们可以把有可能出现数据安全问题的代码使用同步代码块进行包裹(锁住),让任意时刻只能有一个线程执行即可
同步代码块的 语法格式:

synchronized(对象){
	//不能在括号了直接 new 对象 new 了就没效果要被同步的代码 ;
	//这个对象可以使用任意的Java对象代替;
}

使用同步代码块包裹住可能出现数据安全问题的代码,保证在某一时刻只有一个线程可以进来这段代码并持有锁,它一旦持有了这把锁,等到执行完这段逻辑,才会释放锁,让线程们再次争抢CPU资源,来持有这把锁;

具体实现:

package demo11;

public class CellRunnable extends Thread {
    //共享数据,被多个线程所共享。变量共享 一个对象改变这个共享变量值,另一个变量继续使用改变后的值,不会重置。
    //每一个窗口共享总票数,而不是每一个窗口都有100张票
    static int piao = 100;
    //创建一个三个线程共同持有的一把锁对象
    private static Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (piao > 0) System.out.println(this.getName() + "正在出售:" + (piao--) + " 张票");
            }
        }
    }
}

测试类:

package demo11;

public  class test {

        public static void main(String[] args) {
                CellRunnable th = new CellRunnable();
                CellRunnable th1 = new CellRunnable();
                CellRunnable th2 = new CellRunnable();
                th1.setName("窗口1");
                th2.setName("窗口2");
                th.setName("窗口3");
                th1.start();
                th2.start();
                th.start();


        }
}

结果:
在这里插入图片描述

再也没有出现数据安全问题;
这里要注意:三个线程必须使用同一个锁对象,因为只有这样,才能保证某一时间段内只有一个线程可以持有这把锁,其中一个线程抢到了CPU执行权,锁住了门,其他线程只能等在门外,如果三个对象各自使用一把锁,就是各自运行各自的任务,没有任何意义了;

什么是synchronized?

synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对象的所有读写都将通过同步的方式来进行;

synchronized锁的是什么?

普通同步方法 ---------------> 锁的是当前实例对象;
静态同步方法---------------> 锁的是当前类的Class对象;
同步方法块 ---------------> 锁的是synchonized括号里配置的对象;

同步方法与静态同步方法

上面我们看到了如何给代码块加一个锁,其实还有两种锁,可以直接将锁对象加在方法上面,根据方法是否为静态,这两种加锁的方法被称为:同步方法与静态同步方法;

  • 同步方法默认的锁对象是this,代表当前对象,因为我们在测试类里面只new了一个CellRunnable 对象,三个线程使用一个任务对象,因此三个线程每次抢到CPU执行权之后,都是使用这个对象锁住这个方法;
package demo12;

public class CellRunnable implements Runnable {
    //共享数据,被多个线程所共享。变量共享 一个对象改变这个共享变量值,另一个变量继续使用改变后的值,不会重置。
    //创建一个任务开启三个线程去做 因此也是共享变量int piao=100 不需要加static
    int piao = 100;
    @Override
    public  synchronized void  run() {
       while(true){
           try {
               Thread.sleep(20);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (piao>0) System.out.println(Thread.currentThread().getName()+"正在出售:"+(piao--)+" 张票");
       }
    }
}
package demo12;

public  class test {
        /* 需求:某电影院目前正在上映贺岁大片,
       共有100张票,而它有3个售票窗口售票,
       请设计一个程序模拟该电影院售票。
        通过继承Thread类实现*/

        public static void main(String[] args) {
                //创建一个任务开启三个线程去做 因此也是共享变量int piao=100 不需要加static
                CellRunnable cellRunnable = new CellRunnable();
                Thread th = new Thread(cellRunnable);
                Thread th1 = new Thread(cellRunnable);
                Thread th2 = new Thread(cellRunnable);
                th1.setName("窗口1");
                th2.setName("窗口2");
                th.setName("窗口3");
                th1.start();
                th2.start();
                th.start();
        }
}
  • 静态同步方法 sychronized默认持有锁对象是当前类的字节码文件对象,它属于类锁;
----自定义Runnable类:
public class sellRunnable implements Runnable{
    //这个票让三个线程共享
    static int piao = 100; //共享数据

    @Override
    public void run() {
        while (true) {
            //th1 th2 th3
            maiPiao();
            //th1 执行完了,出了同步代码块,就会释放锁。释放锁了之后,多个线程再去争抢CPU的时间片
        }
    }

    //静态同步方法:默认用的锁对对象,用的是当前类的字节码对象
    public static synchronized void maiPiao() {
        if (piao >= 1) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第:" + (piao--) + " 张票");
        }
    }
}

静态同步方法与同步代码块共同使用

静态同步方法的锁是当前类的字节码文件对象,偶数次使用同步代码块内容,奇数次使用静态同步方法内容,如果同步代码块和同步方法的锁不一样线程各自运行各自的任务就会发生线程安全的问题,这时设置同步代码块的锁为字节码文件对象,两把锁一样其他线程就不会在一个线程执行同步代码块/静态同步方法的时候重复执行同步代码块/静态同步方法的内容,就能够锁住。进而说明静态同步方法的锁是当前类的字节码文件对象。

使用同步代码块与静态同步方法,共同模拟卖票案例;

package demo13;

import demo12.CellRunnable;
public class test1 {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        Thread th = new Thread(cellRunnable);
        Thread th1 = new Thread(cellRunnable);
        Thread th2 = new Thread(cellRunnable);
        th1.setName("窗口1");
        th2.setName("窗口2");
        th.setName("窗口3");
        th1.start();
        th2.start();
        th.start();
    }
}

package demo13;

public class CellRunnable implements Runnable {
    //共享数据,被多个线程所共享
    static int piao = 100;
    static Object obj=new Object();
    int i=0;
    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                //同步代码块的锁是任意对象,
                // 而同步方法需要的锁是this,静态同步方法的锁是当前类的字节码文件对象,
                // 因此修改同步代码块的锁为this或者当前类的字节码文件对象,可以运行。
                synchronized (CellRunnable.class) {
                    //模拟真实情况中,网络延迟的现象。
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (piao > 0) System.out.println(Thread.currentThread().getName() + "正在出售:" + (piao--) + " 张票");

                }//释放锁
            } else {
                maiPiao();//释放锁}
            }

            i++;
        }
    }

    private static  synchronized void maiPiao() {
        //就是最后一张。piao=1;
        //加锁
        try {
            //模拟真实情况中,网络延迟的现象。
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (piao > 0) {
            System.out.println(Thread.currentThread().getName() + " 正在出售:" + (piao--) + " 张票");
        }
    }


    //方法上加上关键字synchronized 同步方法
    //同步方法的默认锁对象是this
    private synchronized void maiPiao2() {

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (piao>0
            ) System.out.println(Thread.currentThread().getName() +"正在出售:" + (piao--) + " 张票");

    }
    }

Java中类的线程安全与线程不安全的对比

1、ArraryList 与 Vector 集合
前者是线程不安全,效率高;
后者是线程安全,效率低;
在这里插入图片描述
2、StringBuilder 与 StringBuffer 类

  • 之前讲到 StringBuffer 是线程安全的方法,因为他的方法都是同步方法。
    在这里插入图片描述

  • 如果使用了一个线程不安全的集合但是不想更换的时候可以选择:把线程不安全的集合转换为线程安全的集合

		ArrayList<String> list = new ArrayList<>();
         //把线程不安全的集合转换为线程安全的集合
        List<String> strings = Collections.synchronizedList(list);

由此可见,这些线程安全的类是因为他们的成员方法都加了关键字sychronized,保证了一段时间内只能有一个线程来执行这个方法里面的逻辑,也就保证了数据安全;

同步的弊端

当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题;

Lock锁

  • 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,比较抽象,为了更清晰的表达如何加锁和释放锁,JDK 1.5以后,并发包新增Lock接口来实现锁功能;

  • Lock接口提供 synchronized不具备的主要特性
    在这里插入图片描述

  • 一般使用的是Lock锁的是是实现类:ReentrantLock,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock锁的灵活度要远远大于前者;重入锁:可重新反复进入的锁,但仅限于当前线程;

在这里插入图片描述

  • 从类结构图看出,ReentrantLock 实现了 Lock 接口,ReentrantLock 只是 Lock 接口的一个实现而已。
    在这里插入图片描述
public void m() {
    lock.lock();
    lock.lock();
    try {
      // ... method body
    } finally {
      lock.unlock()
      lock.unlock()
    }
}

  • 如示例代码所示,当前线程可以反复加锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导致锁不被释放;
    试想一下,如果不设计成可重入锁,那自己如果反复给自己加锁,不是会把自己加死锁了吗?

重入锁最重要的几个方法
在这里插入图片描述
1)lock()

获取锁,有以下三种情况:

锁空闲:直接获取锁并返回,同时设置锁持有者数量为:1;
当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1;
其他线程持有锁:当前线程会休眠等待,直至获取锁为止;

2)unlock()

释放锁,每次锁持有者数量递减 1,直到 0 为止。所以,现在知道为什么 lock 多少次,就要对应 unlock 多少次了吧。

class X {

  private final ReentrantLock lock = new ReentrantLock();

  // ...

  public void m() {
    lock.lock();  // block until condition holds
    try {
      // ... method body
    } finally {
      lock.unlock()
    }
  }
}

加锁和释放锁都在方法里面进行,可以自由控制,比 synchronized 更灵活,更方便。但要注意的是,释放锁操作必须在 finally 里面,不然如果出现异常导致锁不能被正常释放,进而会卡死后续所有访问该锁的线程。

synchronized 是重入锁吗?
看下面的例子:

public synchronized void operation(){
    add();
}

public synchronized void add(){

}

operation 方法调用了 add 方法,两个方法都是用 synchronized 修饰的,add() 方法可以成功获取当前线程 operation() 方法已经获取到的锁,说明 synchronized 就是可重入锁。

卖票案例演示:

package org.westos.demo5;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CellRunnable implements Runnable {
    //共享数据,被多个线程所共享。
    int piao = 100;
    static Object obj = new Object();
    static Lock lock=new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //加锁
            lock.lock();
            try{
                if (piao > 0) {
                    try {
                        //模拟真实情况中,网络延迟的现象。
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + " 正在出售:" + (piao--) + " 张票");
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}

死锁现象

指的是两个或两个以上的线程,因为同时抢占CPU资源而出现的相互等待现象;

代码展示:

package demo15;

public class MyTgread extends Thread {
    boolean flag;

    public MyTgread(boolean flag) {
        this.flag = flag;

    }

    @Override
    public void run() {
        // 两个或者两个以上的线程, 在抢占CPU的执行权的时候, 都处于等待状态
        if (flag) {
            synchronized (ObjectUtils.objA) {
                System.out.println("true 线程进来了 持有objA锁");
                //true线程拿了锁A执行sout代码后,需要获取锁B来执行后续代码
                synchronized (ObjectUtils.objB) {
                    System.out.println("true 线程进来了 持有objB锁");
                }
            }//释放锁
        } else {
            synchronized (ObjectUtils.objB) {
                System.out.println("false 线程进来了 持有objB锁");
                //false线程拿了锁B后需要获取锁A来执行后续代码
                synchronized (ObjectUtils.objA) {
                    System.out.println("false 线程进来了 持有objA锁");
                }
            }//释放锁
        }
    }
}

测试类:

 MyTgread th1=new MyTgread(true);
        MyTgread th2=new MyTgread(false);
        th1.start();
        th2.start();

工具类:

public interface ObjectUtils {
    //创建 两把锁对象
   Object objA= new Object();
   Object objB= new Object();
}

执行结果:
在这里插入图片描述

现象解释:

1、true线程最先抢占了CPU资源,持有了A锁;
2、flase线程紧接着抢占了CPU资源,持有了B锁;
3、true线程等待false线程执行完毕释放B锁,false线程等待true线程执行完毕释放A锁,但是他们都处于互相等待状态,谁也不让谁,就出现了死锁现象;
4、这就相当于:中国人和美国人一起吃饭,中国人使用的筷子,美国人使用的刀和叉;中国人获取到了美国人的刀,美国人获取到了中国人的一根筷子,谁也不让谁,谁都吃不成饭;

多线程下的等待唤醒机制

在实际的软件开发过程中,经常会碰到如下场景:

    某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等);
    产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
    单单抽象出生产者和消费者,还够不上是生产者/消费者模式;
    该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介,生产者把数据放入缓冲区,而消费者从缓冲区取出数据;
    下图是对生产者消费者的理解图:
    1、你把信写好——相当于生产者制造数据
    2、你把信放入邮筒——相当于生产者把数据放入缓冲区
    3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区
    4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据
    这个生产者消费者模式也叫做等待——唤醒机制;

在这里插入图片描述
代码模拟:
1、我们使用Student类模拟缓存,使用getThread类模拟消费者,使用setThread类来模拟生产者;
2、首先需要明确,生产者消费者模型希望达到的效果是:生产一个学生对象,消费一个学生对象,也就是set线程的run()需要增加学生信息(比如设置 [“张三”,23]),get线程的run()需要获取学生信息 [获取到 “张三”,23这个学生对象],但是学生对象是new出来的,两个线程之间没办法共享这个数据,怎么才能共享呢?我们可以给两个线程提供一个有参构造,在测试类里面就创建好学生对象,然后将一个学生对象传递给两个线程,这样可以实现学生对象的资源共享;

----学生类:
public class Student {
    public int age;
    public String name;
}

为了体现线程不安全,引入i++

----生产者:
public class setThread  extends Thread {
    int i = 1;
    Student student;

    public setThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true) {
            //没有资源生产资源
            if (i % 2 == 0) {
                //生产资源
                student.name = "张三";
                student.age = 23;
            } else {
                //生产资源 th1
                student.name = "李四";
                student.age = 24;
            }
            i++;
        }
    }
}

-----消费者:
public class getThread extends Thread {
    private Student student;

    public getThread(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        //消费资源
        while (true) {
            //有了资源就消费
            System.out.println(student.name + "===" + student.age);
        }
    }
}

执行结果:
在这里插入图片描述

  • 上面的运行结果可以看出,程序出现了数据安全的问题,本来希望达到生产一个消费一个的效果没有出现,明明生产的是李四–24,张三–23,却出现了李四–23,张三–24的现象;因为线程一旦开启,就会具有随机性,有可能还没生产,就消费了,也有可能生产到一半,就被消费了,造成名字与年龄不匹配;(生产线程抢到时间片,但消费线程没有抢到时间片,i++让生产线程执行else的逻辑,设置name=李四,此时消费线程抢到时间片而此时age没有设置为24 ,消费线程就会打印李四== 23)
  • 怎么解决数据安全的问题呢?可以给生产资源和消费资源的代码加sychronized锁,锁住这部分代码,保证在没有生产好资源的时候,不能消费资源;并且需要注意的是,我们生产和消费线程必须使用的同一个锁住,这样才能保证一段时间内只能执行一个线程的任务,这时候,他们共享资源学生对象可以充当这个锁对象;
  • 但是这样写代码还是不行,因为我们生产者一次只能生产一个资源,但是由于线程的随机性,有可能生产了一个资源,但是却在重复消费这个资源(消费线程不断成功争取到CPU执行权,就会重复消费)
  • 我们怎么解决上面的问题呢?
    在生产线程里面,我们希望生产完成一个资源,消费一个资源;作为生产者来说,我生产一个资源,就通知消费线程来消费这个资源;
    作为消费线程来说,我消了资源,我就等着,并且通知生产线程来生产;
    这时候就需要用到等待——唤醒机制;(指生产线程生产资源,就等待并通知消费线程去消费;消费线程获得生产资源并消费,就通知生产线程生产)

Object 类中:
void wait ()——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待;
作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入"预执行队列"中,并在 wait()所在的代码处停止执行,直到接到通知或中断为止
在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁
void wait (long timeout)——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待;
void notify ()——唤醒在此对象监视器上等待的单个线程
notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发出通知 notify,并让它等待获取该对象的对象锁
void notifyAll ()——唤醒在此对象监视器上等待的所有线程;
notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

  • 考虑如何判断是否已经生产好了学生对象,因为不能出现还没生产就消费了的现象,可以给学生对象加一个布尔类型的值作为标记:
    如果没有资源,使得当前线程处于等待状态;如果有资源就消费该学生资源,将标记置为false,并通知生产者;如果资源已经生产好了,就等待消费者消费,如果没有资源就生产资源并去通知消费,也就是唤醒;这里使用共享资源Student资源让线程等待或者唤醒;

代码执行逻辑:

  • 假设消费者先抢到资源,同步代码块中,首先flag=false,消费线程等待就会释放锁,消费者生产者接着争抢资源,如果生产线程抢到资源,生产线程生产资源,并将标记改为true,并唤醒消费线程,它们继续争抢资源;如果消费者抢到资源,flag=false,依旧等待释放锁,它们继续争抢资源

  • 假设生产者先抢到资源,flag=false,就会生产资源后,令flag=true,唤醒等待的消费者,生产者消费者开始争夺资源,如果消费者抢到,flag=true 消费,唤醒生产者生产,生产者消费者开始争夺资源;如果消费者没抢到,由于flag=true生产者依旧会等待,生产者消费者继续争夺资源。

代码设计思路: 先要决定什么时候是等待的条件,这样避免线程不安全的问题。

  • 对于消费者来说:如果没有资源,使得当前线程处于等待状态;如果有资源就消费该学生资源,将标记置为false,并通知生产者;
  • 对于生产者来说:如果资源已经生产好了,就等待消费者消费,如果没有资源就生产资源并去通知消费,也就是唤醒;这里使用共享资源Student资源让线程等待或者唤醒;
package demo14;
//生产线程抢到时间片,但消费线程没有抢到时间片,i++让生产线程执行else的逻辑,设置name=scq1,此时消费线程抢到时间片而此时生产线程还没有把age设置为11 ,因此消费线程就会打印scq1== 10

//因此使用同步代码块避免多线程不安全的问题,但是要注意多个线程共享一把锁对象。

//新的需求:为了让生产线程与消费线程通信,生产再消费 交替进行:
// 使用线程等待唤醒机制,指没有资源时生产线程生产资源通知消费线程去消费,有资源时就等待;消费线程有资源时获得生产资源并消费,没有资源时就等待,通知生产线程生产。

public class SetThread extends Thread {
    private Student student;

    public SetThread(Student student) {
        this.student = student;
    }

    int i = 0;

    @Override
    public void run() {
        while (true) {


                synchronized (student) {
                    if (student.flag) {
                        try {
                            student.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (i % 2 == 0) {
                        student.name = "张三";
                        student.age = 23;
                    } else {
                        student.name = "李四";
                        student.age = 24;
                    }
                    student.flag = true;
                    student.notify();

                }
                //不确定的代码放在同步代码块中,i在同步代码块外,确定了i,才能确定生产哪种学生对象
                i++;
            
        }
    }
}


package demo14;

public class GetThread extends Thread {
    private Student student;

    public GetThread(Student student) {
        this.student = student;
    }


    @Override
    public void run() {
        while (true) {

            synchronized (student) {
                //如果没有资源,消费者就等待
                if (!student.flag) {
                    try {
                        //线程一旦等待,就立马释放释放锁,如果再次被唤醒,就从这里继续执行。
                        student.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //如果有资源 消费者消费,消费后打上资源为false的标记
                //消费资源
                System.out.println(student.age + "==" + student.name);
                //
                student.flag = false;
                student.notify(); //唤醒正在等待的线程,唤醒之后,他们还要再次争抢。
            }
        }
    }
}

package demo14;

public class Student {
    String name;
   Integer age;
    public  boolean flag=false;//false表示没有资源,true表示有资源。
}

注意:wait()只有notify()才能唤醒它;
在哪里等待,被唤醒后,就从这里执行;
即使被唤醒之后,线程还是会去争抢资源,拿set线程来说,即使它被唤醒后还是会抢占CPU资源,但是由于我们的代码增加了判断条件,如果有资源就处于等待,这样的话,get线程总会抢到资源去消费;
线程一旦等待,就会立马释放锁;
等待线程被唤醒之后,并不会立刻执行,因为唤醒线程还持有"该对象的同步锁",必须等到唤醒线程释放了"对象的同步锁"之后,等待线程才有可能获取到"对象的同步锁",进而继续执行;
wait()和notify()只有在同步中才有效;
唤醒的意思就是让线程具备执行资格;

wait()和sleep()的区别

  • 共同点:都可以使得线程处于阻塞状态;
  • 不同点:
    sleep() 线程一旦休眠 不释放同步锁。
    wait() 线程一旦等待会立马释放锁
    sleep() 必须设置休眠时间
    wait() 可以设置等待时间,也可以不设置。

内存可见性问题

看下面这样一个现象:
自定义线程中 定义一个获取flag变量的方法,主线程中获取子线程修改的变量flag,此时主线程无法获取到修改后的flag=true的变量,因此一直无法退出死循环。

public class test {
  
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

        while (true){
            if (myRunnable.getFlag()){
                System.out.println("当flag为"+myRunnable.getFlag()+"主线程执行");
                break;
            }
        }


    }
}

class MyRunnable implements Runnable{
    public boolean flag=false;

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("线程把flag改为"+flag);

    }
}

执行结果:
在这里插入图片描述

  • 这是为什么?明明线程已经进来执行了任务,这意味着他也修改了标记值为true,那为什么主线程认为这个标记值为false?
    这是由于内存可见性问题,Java的内存模型分为主存和每个线程都具有的工作内存,操作主存中的数据就需要将主存中的数据进行拷贝,修改完之后再刷回主存;

在这里插入图片描述
由于这个写回主存的时间具有不确定性,因此具有随机性,但是主程序很可能很早就读取了这个成员变量的值,造成了数据安全的问题;
为了避免这个错误,我们可以给程序加一个sychronized关键字,保证程序顺利执行;就是让子线程与主线程执行的代码块都使用同一把锁对象,就可以保证程序的执行顺序是正常的

为什么可以顺利执行的原因:
Java程序在编为字节码文件的时候,代码的执行顺序有可能和写的顺序不一样,也就是不知道什么时候flag会写回主存,所以造成了上面的代码出现卡死;
上面的程序加锁应该给读数据、写数据都要加锁;
Sychronized关键字还有一个作用是 确保变量的内存可见性,写数据的时候读数据线程去申请同一个锁,它的工作内存会被设置为无效,然后读线程会重新从主存中加载它要访问的变量到它的工作内存中;
因此就保证了读完之后再写上去程序顺利执行;

public class test {

    //自定义线程中 定义一个获取flag变量的方法,主线程中获取子线程定义的变量
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

        while (true) {
            synchronized (MyRunnable.objectA) {
                if (myRunnable.getFlag()) {
                    System.out.println("当flag为" + myRunnable.getFlag() + "主线程执行");
                    break;

                }
            }
        }


    }
}

class MyRunnable implements Runnable {
   boolean flag = false;
   static Object objectA=new Object();

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {
synchronized (objectA) {
    try {
        Thread.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    flag = true;
    System.out.println("线程把flag改为" + flag);
}

    }
}

加入sychronized关键字也可以,但是这样一来程序可以顺利执行,但是这样造成效率一定会降低,有没有更好的办法?我们可以引入Volatile关键字;

1、Volatile关键字是轻量级的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,它保证了 内存的可见性问题;
2、 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性;

public class test {

    //自定义线程中 定义一个获取flag变量的方法,主线程中获取子线程定义的变量
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

        while (true) {

            if (myRunnable.getFlag()) {
                System.out.println("当flag为" + myRunnable.getFlag() + "主线程执行");
                break;

            }
        }


    }
}

class MyRunnable implements Runnable {
    volatile boolean flag = false;

    public boolean getFlag() {
        return this.flag;
    }

    @Override
    public void run() {

        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("线程把flag改为" + flag);


    }
}

执行结果:
在这里插入图片描述

另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性;
volatile 关键字:相较于 synchronized 是一种较为轻量级的同步策略;
volatile 变量,用来确保将变量的更新操作通知到其他线程;保证被线程修改的值立刻刷回主存,其他线程可以在主存中获得最新的值而不是没有被刷新的旧值

可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的"原子性操作"
    原因如下:

Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了;
3、举例:
①线程A首先得到了 i 的初始值100,但是还没来得及修改,就阻塞了;
②这时线程B开始了,它也得到了 i 的值,由于 i 的值未被修改,即使是被volatile 修饰,主存的变量还没变化,那么线程B得到的值也是100;
③之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中;
④根据可见性的原则,这个主存的值可以被其他线程可见;
⑤问题来了,线程A已经读取到了 i 的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存;
所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

CAS算法

由于i++操作是一个非原子性操作,因此出现了数据安全问题;
如何解决呢?sychronized是一个重量级的,volatile不能保证原子性,这时候需要用到 CAS算法;
CAS 包含了 3 个操作数:需要读写的内存值 V、进行比较的值 A、拟写入的新值 B;
当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作;

CAS让多个线程并发去访问但不产生原子性的问题,预估值与主存值不相等表示有线程已经更新过此值就不会继续更改
在这里插入图片描述

上面的图可知,两个线程其实就是在比较谁将新值刷回主存更快,你读到的不一定是此时内存中的值;
假如有三个线程,都读取到了主存中的100作为预估值,并进行修改操作,修改为101,但是线程1刷回主存很快,将101写入主存,其他两个线程将预估值和主存中的值一对比,对不上,因此刷回失败;就是在比较谁刷的快;

CAS应用
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
AtomicIntegerArray 、 AtomicLongArray
AtomicMarkableReference
AtomicReferenceArray

我们只需要把变量对应的类型换为上面的类型就好了,这些类型里面比如AtomicInteger 提供的自加、自减操作都可以确保原子性操作;
在这里插入图片描述

import java.util.concurrent.atomic.AtomicInteger;

public class test {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }

}
class  MyRunnable implements  Runnable{
    //int i=1;
   AtomicInteger i= new AtomicInteger(1);

    @Override
    public void run() {
        while (i.get()<10){
           //i++
            int andIncrement = i.getAndIncrement();
            System.out.println(andIncrement);
        }

    }
}

在这里插入图片描述

匿名内部类开启线程

public class test {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("开启子线程");
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("开启子线程");
            }
        }).start();
    }
}

线程池

概述
1、程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互;
2、而使用线程池可以很好的提高性能,他本质就是一个存储一定数量线程对象的容器,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池;
3、线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
4、在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池;

线程池就是预存很多线程,获取任务执行任务后处于就绪状态而不销毁继续获取任务
创建一个线程池
1、 JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法

public static ExecutorService newCachedThreadPool():
根据任务的数量来创建线程对应的线程个数
public static ExecutorService newFixedThreadPool(int nThreads):
固定初始化几个线程
public static ExecutorService newSingleThreadExecutor():
初始化一个线程的线程池

2、这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:
在这里插入图片描述
线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程直到任务完成;get(long timeout,TimeUnit unit)可以设置超时时间;

3、创建线程池使用步骤:

创建线程池对象
创建Runnable实例
提交Runnable实例
关闭线程池

4、为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池;
降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。
提供线程可以管理性:可以通过设置合理分配、调优、监控。

5、线程池工作流程

(1)判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务,是->走下个流程。
(2)判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。
(3)判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,是->走下个流程。
(4)按照设置的策略来处理无法执行的任务。

import java.util.concurrent.*;

public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //根据任务的数量来创建线程对应的线程个数
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new MyRunnable());
        executorService.submit(new MyRunnable());
        executorService.submit(new MyRunnable());


        MyCallable myCallable = new MyCallable();
        //Future类接收处理返回值 并使用get方法获取返回值
        Future<Integer> future = executorService.submit(myCallable);
        System.out.println(future.get());

        //关闭线程池
        executorService.shutdown();


        // public static ExecutorService newFixedThreadPool ( int nThreads):固定初始化几个线程
        //只会由3个线程来轮回执行这5个任务
        ExecutorService executorService1 = Executors.newFixedThreadPool(3);
        executorService1.submit(new MyRunnable());
        executorService1.submit(new MyRunnable());
        executorService1.submit(new MyRunnable());
        executorService1.submit(new MyRunnable());
        executorService1.submit(new MyRunnable());

        executorService1.shutdown();

        //public static ExecutorService newSingleThreadExecutor ():初始化一个线程的线程池
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();
        //只会由这一个线程来轮次执行以下四个任务
        executorService2.submit(new MyRunnable());
        executorService2.submit(new MyRunnable());
        executorService2.submit(new MyRunnable());
        executorService2.submit(new MyRunnable());

        executorService2.shutdown();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"子线程执行");
    }
}
class MyCallable implements Callable<Integer>{
    int num=10;
    @Override
    public Integer call() throws Exception {
        return num*3;
    }
}

在这里插入图片描述

定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行;
在Java中,可以通过Timer和TimerTask类来实现定义调度的功能;

Timer:
public Timer()
public void schedule(TimerTask task, long delay):
--------------------安排在指定的时间执行指定的任务
public void schedule(TimerTask task,long delay,long period);
--------------------task - 所要安排的任务。
--------------------delay - 执行任务前的延迟时间,单位是毫秒。
--------------------period - 执行各后续任务之间的时间间隔,单位是毫秒。
public void schedule(TimerTask task, Date time):
public void schedule(TimerTask task, Date firstTime, long period):

TimerTask:定时任务

public abstract void run()
public boolean cancel()
在这里插入图片描述

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class test{
    public static void main(String[] args) throws ParseException {
        //创建一个定时器
        //一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
        Timer timer = new Timer();
        //TimerTask由 Timer 安排为一次执行或重复执行的任务。
        //public void schedule (TimerTask task,long delay):
        //延迟两秒执行这个任务
        timer.schedule(new MyTimerTask(timer),2000);

        // public void schedule (TimerTask task,long delay, long period);
        //第一次延迟两秒执行,以后间隔1秒重复执行。
        //timer.schedule(new MyTimerTask(), 2000,1000);
        
        //可以设置具体的年月日来执行定时任务
        String dateStr="2022-08-21 14:44:00";
        Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
        timer.schedule(new MyTimerTask(timer),date);
        
    }
}

class MyTimerTask extends TimerTask{
    private Timer timer;

    public MyTimerTask(Timer timer) {

        this.timer = timer;
    }

    @Override
    public void run() {
        System.out.println("定时任务执行");
        //取消定时器
        timer.cancel();
    }
}

定时删除一个文件夹

import java.io.File;
import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;

public class test{
    public static void main(String[] args) throws ParseException {
        File file = new File("C:\\Users\\Administrator\\Desktop\\dd");
        Timer timer = new Timer();
        DelFileTask delFileTask = new DelFileTask(timer,file);

        //2s后删除此文件夹
        timer.schedule(delFileTask,2000);


    }
}

class DelFileTask extends TimerTask{


    private Timer timer;
    private File file;

    public DelFileTask(Timer timer,File file) {

        this.timer = timer;
        this.file = file;
    }

    @Override
    public void run() {
       delFolder(file);
        timer.cancel();
    }

    private void delFolder(File file1) {

        File[] files = file1.listFiles();
        for (File f : files) {
            if (f.isFile())f.delete();
            else {
                delFolder(f);
            }
        }
        file1.delete();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值