Java基础知识(三十二)多线程(二)

目录

一、多线程

1、Lock锁

2、死锁

二、线程间通信

1、等待唤醒机制

2、线程组

3、线程池

实现线程池

实现Callable接口

匿名内部类的形式实现多线程

 定时器


一、多线程

1、Lock锁

同步代码块和同步方法虽然解决了线程安全问题,但表达不出在哪里加了锁,在哪里释放了锁,所以在JDK1.5之后提供了一个新的锁对象:Lock

Lock是一个接口,其中有两个方法:

void lock():加锁 ;void lock():释放锁

在创建Lock对象需要使用他的实现类:ReentrantLock

以电影院卖票为例:也就是解决线程安全问题的第二种方式

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class RunableDemo2 implements Runnable{
    private int ticket=100;
    private Lock lock=new ReentrantLock();
    @Override
    public void run() {
        while(true){
            lock.lock();
            if(ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
            }
            lock.unlock();
        }
    }
}

测试类:

public class TicketDemo2 {
    public static void main(String[] args) {
        RunableDemo2 runableDemo2 = new RunableDemo2();
        Thread t1 = new Thread(runableDemo2);
        Thread t2 = new Thread(runableDemo2);
        Thread t3 = new Thread(runableDemo2);
        t1.setName("大黑");
        t2.setName("二黑");
        t3.setName("三黑");
 
        t1.start();
        t2.start();
        t3.start();
    }
}

输出结果和之前的结果相同

2、死锁

是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

这也是同步的弊端之一:效率低;如果出现了同步嵌套,很容易产生死锁问题。

代码举例:

创建两个锁对象的类:

public class Dlock {
    public static final Object lock1 = new Object();
    public static final Object lock2 = new Object();
}

实体类:

public class Ddemo1 extends Thread{
   private boolean flag;
   public Ddemo1(boolean flag){
       this.flag=flag;
   }
    @Override
    public void run() {
        if(flag){
            synchronized (Dlock.lock1){
                System.out.println("if lock1");
                synchronized (Dlock.lock2){
                    System.out.println("if lock2");
                }
            }
        }else{
            synchronized (Dlock.lock2){
                System.out.println("else lock2");
                synchronized (Dlock.lock1){
                    System.out.println("else lock1");
                }
            }
        }
    }
}

测试类:

public class Ddemotest {
    public static void main(String[] args) {
        Ddemo1 ddemo1 = new Ddemo1(true);
        Ddemo1 ddemo2 = new Ddemo1(false);
 
        ddemo1.start();
        ddemo2.start();
    }
}

结果:会有四种结果,两种结果正常,两种结果发生了死锁

正常结果:运行之后程序直接结束

  不正常结果:发生了死锁现象:两个线程同时进入,在执行完第一个同步操作之后,由于互相之间需要的另一个锁对象正在被调用,所以两个线程在等待互相释放锁对象,因此发生了死锁,将永久等待。运行之后程序将一直运行,不会停止。

二、线程间通信

针对同一个资源的操作有不同种类的线程

举例:学生前去电影院看电影需要买票,当票卖完之后,电影院需要再次生产电影票。

按照正常分析:需要创建两个实体类去实现Runable接口,一个表示生产者,一个表示消费者,再创建一个学生对象类,表示两个实体类是对该对象进行操作,在两个实体类中都创建学生对象,表示对学生对象进行操作。

但这样输出结果永远都是默认值null和0,因为两个实体类中所进行操作的对象不是同一个对象。

改进:在外界创建一个对象,通过参数的形式传入两个实体类中进行操作。加入循环和另一个学生对象使结果更清楚:

代码实现:

实体类:get类

public class Stget implements Runnable{
    private Student s;
    public Stget(Student s){
        this.s=s;
    }
    @Override
    public void run() {
        while(true){
            System.out.println(s.name+"---"+s.age);
        }
 
    }
}

 set类:

public class Stset implements Runnable{
    private Student s;
    private int i=0;
    public Stset(Student s){
        this.s=s;
    }
    @Override
    public void run() {
        while(true){
            if (i%2==0){
                s.name="大黑";
                s.age=18;
            } else{
                s.name="大白";
                s.age=20;
            }
            i++;
        }
 
 
    }
}

学生类:

public class Student {
    public String name;
    public int age;
}

 测试类:

public class Stdemo1 {
    public static void main(String[] args) {
//        在外界创建学生对象,通过参数的形式传入线程类中
        Student s = new Student();
        Stget stget = new Stget(s);
        Stset stset = new Stset(s);
 
        Thread t1 = new Thread(stget);
        Thread t2 = new Thread(stset);
 
        t1.start();
        t2.start();
    }
}

输出结果发现:

两个学生对象的名字和年龄会出现了对不上和重复的情况 

解决办法:

满足线程安全问题的三个条件,所以需要加入synchronized关键字加锁或者Lock锁

代码完善:

加入synchronized加锁:

get类:

public class Stget implements Runnable{
    private Student s;
    public Stget(Student s){
        this.s=s;
    }
    @Override
    public void run() {
        while(true){
            synchronized (s){
                System.out.println(s.name+"---"+s.age);
            }
 
        }
 
    }
}

set类:

public class Stset implements Runnable {
    private Student s;
    private int i = 0;
 
    public Stset(Student s) {
        this.s = s;
    }
 
    @Override
    public void run() {
 
        while (true) {
            synchronized (s) {
                if (i % 2 == 0) {
                    s.name = "大黑";
                    s.age = 18;
                } else {
                    s.name = "大白";
                    s.age = 20;
                }
                i++;
            }
        }
 
    }
}

输出结果就不会出现名称与年龄不匹配的情况了,但此时出现了新的问题:一个数据操作了多次

电影院卖票是在电影院有票的情况下才会卖,若是没有票则应该先生产票在卖,消费者也是如此,卖了一张票之后,继续买票,若电影院没有票了,则应该等待电影院生产票之后再买。

这种生产消费者的问题,使用Java提供的等待唤醒机制解决

1、等待唤醒机制

如何添加等待唤醒机制呢? 所需要调用的方法在Object类中,Object类中有三个方法需要我们学习。

void notify() 唤醒正在等待对象监视器的单个线程。

void notifyAll() 唤醒正在等待对象监视器的所有线程。

void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。

为什么不在Thread方法中定义这三种方法:

这些方法要想调用,必须通过锁对象调用,因为如果连锁对象都不一样了,就没必要等待唤醒了,直接执行逻辑代码。 而说到现在同步代码块的锁对象是任意对象,类型无法确定,所以这些方法都定义在Object类中,因为Java所有的类都有一个共同父类Object

 代码实现:

set类:

public class Stset implements Runnable {
    private Student s;
    private int i = 0;
 
    public Stset(Student s) {
        this.s = s;
    }
 
    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                if(s.flag){//若有值,则等待消费者消费
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (i % 2 == 0) {
                    s.name = "大黑";
                    s.age = 18;
                } else {
                    s.name = "大白";
                    s.age = 20;
                }
                i++;
                s.notify();
                s.flag=true;
            }
        }
 
    }
}

 get类:

public class Stget implements Runnable{
    private Student s;
    public Stget(Student s){
        this.s=s;
    }
    @Override
    public void run() {
        while(true){
            synchronized (s){
                if(!s.flag){//若没有值,则等待生产者产值
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name+"---"+s.age);
                s.notify();//消费完唤醒生产者
                s.flag=false;//将值改为没有值
            }
 
        }
 
    }
}

输出结果:这样就能实现电影院生产一张票消费者就买一张票

2、线程组

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

代码举例:

先写一个实体类实现Runnable接口,用于创建线程对象

测试类:

public class TGdemo {
    public static void main(String[] args) {
        TGrunnable tGrunnable = new TGrunnable();
        Thread t1 = new Thread(tGrunnable, "大黑");
        Thread t2 = new Thread(tGrunnable, "二黑");
 
        //默认情况下,所有的线程属于主线程
        ThreadGroup tg1 = t1.getThreadGroup();
        ThreadGroup tg2 = t2.getThreadGroup();
        System.out.println(tg1);
        System.out.println(tg2);
 
        //获取线程组的名称
        String name1 = t1.getThreadGroup().getName();
        String name2 = t2.getThreadGroup().getName();
        System.out.println(name1);
        System.out.println(name2);
 
        //创建新的线程组
        ThreadGroup tg3 = new ThreadGroup("白色组");
 
        //创建对象并分组
        Thread t3 = new Thread(tg3, tGrunnable, "大白");
        Thread t4 = new Thread(tg3, tGrunnable, "二白");
        Thread t5 = new Thread(tg3, tGrunnable, "三白");
 
        //获取线程组的名字
        System.out.println(t1.getName()+"属于线程组:"+t1.getThreadGroup().getName());
        System.out.println(t2.getName()+"属于线程组:"+t2.getThreadGroup().getName());
        System.out.println(t3.getName()+"属于线程组:"+t3.getThreadGroup().getName());
        System.out.println(t4.getName()+"属于线程组:"+t4.getThreadGroup().getName());
        System.out.println(t5.getName()+"属于线程组:"+t5.getThreadGroup().getName());
 
        //Java允许下程序直接对线程组进行控制
        //将一个线程组设置为守护线程
        tg3.setDaemon(true);
    }
}

输出结果:

3、线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

实现线程池

1、创建线程池对象,Executors工厂类下有很多获取线程池的静态方法。

newFixedThreadPool是其中的一个线程池

创建线程池:

public static ExecutorService newFixedThreadPool(int nThreads)

nThread指的是可存放线程的数量,若提交的线程数量多于线程池容量,则会等待直到线程池中有线程执行完毕有空闲线程位置时在执行,大体还是会执行。

线程池的一些基本操作:

存放和运行线程:

Future<?> submit(Runnable task)

提交一个可运行的任务执行,并返回一个表示该任务的未来。

即提交就运行,且底层源码将该任务封装成了一个线程对象并启动运行。

手动结束线程的运行:

void shutdown()

启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。

该方法也就是关闭线程池。

代码举例:

实现类:

public class TGrunnable implements Runnable{
    @Override
    public void run() {
        for(int i=1;i<=3;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

 测试类:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Pdemo {
    public static void main(String[] args) {
        TGrunnable tGrunnable = new TGrunnable();
 
        //创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);
 
        //提交线程并运行
        pool.submit(tGrunnable);
        //提交一次就是一个新的线程
        pool.submit(tGrunnable);
        //提交第三个线程,此时该任务仍然会执行
        pool.submit(tGrunnable);
        //停止线程运行,有序关闭线程池,关闭前的线程将会执行
        pool.shutdown();
    }
}

 输出结果:

这里看出,线程2与线程1抢占调度,线程2先执行完毕,此时线程3提交到线程2位置和线程1进行抢占调度。最终全部执行完毕。

解决电影院买票线程安全问题的第三种方式

实现Callable接口

自定义类实现Callable接口,实现call方法,该线程的启动必须与线程池结合,单独无法创建线程对象启动

实现方法和操作线程池类似

<T> Future<T> submit(Callable<T> task)

提交值返回任务以执行,并返回代表任务待处理结果的Future。

在自定义类中实现卖票的具体实现代码

代码实现:

实现类:

import java.util.concurrent.Callable;
 
public class CallableImpl implements Callable {
    private  int ticket=100;
    @Override
    public Object call() {
        while(true){
           synchronized (this){
               if(ticket>0){
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
               }
           }
 
        }
    }
}

测试类:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Cdemo {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);
        //创建实体类对象
        CallableImpl callable = new CallableImpl();
 
        //提交三个线程分别代表三个窗口
        pool.submit(callable);
        pool.submit(callable);
        pool.submit(callable);
        pool.shutdown();
    }
}

 输出结果:

Callable接口的优点:

1、可以有返回值

2、可以抛出异常

弊端:

代码比较复杂,故一般不用 

匿名内部类的形式实现多线程

new Thread(){代码…}.start();继承Thread类重写run方法

New Thread(new Runnable(){代码…}).start();实现Runnable接口重写run方法

代码举例:

public class NmDemo {
    public static void main(String[] args) {
        //继承Thread类,重写run方法,启动线程
        new Thread("大黑"){
            @Override
            public void run() {
                for (int i=1;i<=3;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
 
        //实现Runnable接口,重写run方法,启动线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=1;i<=4;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        },"二黑").start();
    }
}

 输出结果:

 

 定时器

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

创建定时器:

Java提供了一个类给我们使用实现定时器:Timer

定时器中的方法:

void schedule(TimerTask task, long delay)

在指定的延迟delay之后安排指定的任务task执行。

void schedule(TimerTask task, long delay, long period)

在指定的延迟delay之后开始 ,重新执行固定延迟period执行的指定任务task。

代码举例:

import java.util.Timer;
import java.util.TimerTask;
 
public class Tdemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new Task1(timer),3000);
    }
}
 
class Task1 extends TimerTask{
    private Timer timer;
 
    public Task1(Timer timer) {
        this.timer = timer;
    }
 
    @Override
    public void run() {
        System.out.println("beng!!!!!");
    }
}

输出结果:三秒后输出beng!!!

延迟执行举例:

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
 
public class TimerDemo2 {
    public static void main(String[] args) {
        //创建定时器对象
        Timer timer = new Timer();
 
        //void schedule(TimerTask task, long delay, long period)
        //在指定 的延迟之后开始 ,重新执行 固定延迟执行的指定任务。
        //3秒后执行任务,并且之后每隔两秒执行一次
        timer.schedule(new MyTask2(),3000,2000);
    }
}
 
class MyTask2 extends TimerTask{
 
    @Override
    public void run() {
        try {
            FileReader fr = new FileReader("ddd.txt");
            BufferedReader br = new BufferedReader(fr);
            String s = br.readLine();
            System.out.println(s);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值