×

java基础thread——java5之后的多线程(浅尝辄止)

96
潇潇漓燃
2018.06.03 21:24* 字数 1625

承上启下

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

一、JDK5中Lock锁的使用

void lock() 上锁

void unlock() 释放锁

代码示意:

public class SellTicket implements Runnable {
    private int ticket = 20;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
                lock.lock();
                if (ticket <= 0) {
                    break;
                }
                //卖票这个动作不安全。
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "票");
                lock.unlock();
                System.out.println(Thread.currentThread().getName()+"结束");
            }

    }
}

首先我们要造一个锁

Lock lock = new ReentrantLock();

然后调用lock.lock()和lock.unlock()将需要上锁的代码包起来。

但是查看java的一些源码,还是synchronized用的多。

虽然线程有了锁解决了安全问题,但是偶尔也会因为失误操作出现死锁的情况。

同步弊端:

  • 效率低
  • 如果出现了同步嵌套,就容易产生死锁问题

什么是死锁:

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

示例:

首先造两个锁:

public class MyLock {
    // 创建两把锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

同步代码块嵌套:

public class DieLock extends Thread {
    private boolean flag;
    public DieLock(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        } else {
            synchronized (MyLock.objB) {
                System.out.println("else objB");
                synchronized (MyLock.objA) {
                    System.out.println("else objA");
                }
            }
        }
    }
}

测试:

public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}

运行打印:
if objA
else objB

二、线程间通信

生产者、消费者模式:

生产者没有就生产,有就等待消费者消费;消费者有就消费,没有就等待生产者生产。

java提供了等待唤醒的机制。

Object类中提供了三个方法:

wait():等待

notify():唤醒单个线程

notifyAll():唤醒所有线程

代码示例:

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

生产者:

public class SetThread implements Runnable {
    private Student s;
    public SetThread(Student s){
        this.s = s;
    }
    private int x = 0;
    @Override
    public void run() {
        while (true){
            synchronized (s) {
                if (s.flag){
                    try {
                        s.wait();//t1等着,释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (x % 2 == 0) {
                    s.age=20;
                    s.name="徐繁韵";
                } else {
                    s.age=21;
                    s.name="唐富平";
                }
                x++;
                s.flag=true;
                s.notify();//唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
            }
            //t1有,或者t2有
        }
    }
}

消费者:

public class GetThread implements Runnable {
    private Student s;
    public GetThread(Student s){
        this.s = s;
    }
    @Override
    public void run() {
        while (true){
            synchronized (s) {
                if (!s.flag){
                    try {
                        s.wait();//t2等待,立即释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name + ":" + s.age);
                s.flag=false;
                s.notify();//唤醒t1,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
            }
        }

    }
}

测试:

public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);
        t1.start();
        t2.start();
    }
}

输出打印:
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
。
。
。

看的出是生产一条消费一条。为了实现线程间的通信,将共同操作的数据通过有参构造器传入线程。

思考一个问题,为什么等待唤醒的方法不定义在Thread里呢?

这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。所以,这些方法必须定义在Object类中。

栗子优化:

既然wait()、notify()、notifyAll()定义在锁对象里,那么我们把前面的栗子优化一下。

把Student的成员变量给私有化,把设置和获取的操作给封装成功能,并加上同步。设置或者获取的线程里面只需要调用方法即可。

public class Student {
    private String name;
    private int age;
    boolean flag;

    public synchronized  void  set(String name,int age){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        this.age = age;
        this.flag = true;
        this.notify();
    }

    public synchronized  void get(){
        if (!this.flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.name+":"+this.age);
        this.flag = false;
        this.notify();
    }
}


public class SetThread implements Runnable {
    private Student s;
    public SetThread(Student s){
        this.s = s;
    }
    private int x = 0;
    @Override
    public void run() {
        while (true){
                if (x % 2 == 0) {
                   s.set("徐繁韵",20);
                } else {
                   s.set("唐富平",21);
                }
                x++;
            }
        }
}


public class GetThread implements Runnable {
    private Student s;
    public GetThread(Student s){
        this.s = s;
    }
    @Override
    public void run() {
        while (true){
            s.get();
        }
    }
}

public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);
        t1.start();
        t2.start();
    }
}

线程的状态转换图:

status.png

三、线程组:

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

查看我们平时创建的线程默认是属于哪个组:

        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my,"线程一");
        Thread t2 = new Thread(my,"线程二");
        System.out.println(t1.getThreadGroup().getName());//main
        System.out.println(t2.getThreadGroup().getName());//main
        System.out.println(Thread.currentThread().getThreadGroup().getName());//main

可以看出主线程和我们创建的线程都默认属于main线程组。

接下来我们自定义线程组:

        //创建一个线程组
        ThreadGroup tg = new ThreadGroup("dev");
        MyRunnable my = new MyRunnable();
        创建线程时分配组
        Thread t1 = new Thread(tg,my,"线程一");
        Thread t2 = new Thread(tg,my,"线程二");
        System.out.println(t1.getThreadGroup().getName());//dev
        System.out.println(t2.getThreadGroup().getName());//dev

线程组可以统一管理:

        tg.setDaemon(true);
        tg.interrupt();
        tg.destroy();
        tg.isDestroyed();
        tg.isDaemon();

四、线程池:

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

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

JDK5新增了一个Executors工厂类来产生线程池

有如下几个方法:

public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()

这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:

Future<?> submit(Runnable task)

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

代码示意:

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

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //创建线程池,大小为2
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //将实现了接口Runnable的线程放到线程池里运行
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.shutdown();//关闭线程池

    }
}

因为线程池的出现,实现线程的方式有了第三种。

实现Callable接口

public class MyCallable implements Callable {
    private String name;

    public MyCallable(String name){
        this.name = name ;
    }
    @Override
    public Object call() throws Exception {
        Thread.currentThread().setName(this.name);
        for (int i = 0; i <10 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
        return null;
    }
}

public class ThreadCallableDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(new MyCallable("call一号"));
        pool.submit(new MyCallable("call二号"));
        pool.shutdown();

    }
}

这种方式必须依赖线程池实现。可以看出在线程池中,Runnable和Callable两种方式基本相同,不同的是Callable接口是支持泛型的,call()也是有返回值,返回值类型是泛型的类型。

简单应用:

//计算1-n之后
public class MyCallable implements Callable<Integer> {
    private Integer num;
    public MyCallable(Integer num){
        this.num = num;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <=num; i++) {
            sum +=i;
        }
        return sum;
    }
}


public class ThreadCallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        Future<Integer> f2= pool.submit(new MyCallable(200));
        System.out.println(f1.get());
        System.out.println(f2.get());
        pool.shutdown();
    }
}

五、匿名内部类方式使用多线程

在现实使用中,有时候没必要新建一个实现Runnable接口的类来创建线程,我们可能把线程用完就丢了,这样就用到了匿名内部类方式的线程。

代码示意:

/*
 * 匿名内部类的格式:
 *      new 类名或者接口名() {
 *          重写方法;
 *      };
 *      本质:是该类或者接口的子类对象。
 */
public class ThreadNimingDemo {
    public static void main(String[] args) {
    // 继承Thread类来实现多线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"---"+i);
                }
            }
        }.start();
        // 实现Runnable接口来实现多线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"---"+i);
                }
            }
        }){}.start();

        //高难度的错误示范
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("hello"+"---"+i);
                }
            }
        }){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("word"+"---"+i);
                }
            }
        }.start();//执行的是word,这个算是面试题吧,但是这是错误用法,现实中不会出现的。
    }
}

六、多线程总结:

在多线程的面试中经常会问到这些问题:

1:多线程有几种实现方案,分别是哪几种?

两种。

继承Thread类
实现Runnable接口

扩展一种:实现Callable接口。这个得和线程池结合。

2:同步有几种方式,分别是什么?

两种。

同步代码块

同步方法

3:启动一个线程是run()还是start()?它们的区别?

start();

run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法

4:sleep()和wait()方法的区别

sleep():必须指定时间;不释放锁。

wait():可以不指定时间,也可以指定时间;释放锁。

5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中

因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。

6:线程的生命周期图

新建 -- 就绪 -- 运行 -- 死亡

新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡

建议:画图解释。

此次线程的学习,只是简单的涉及,线程间的通信、线程池等并没有深入探究。在经后实际项目的高并发的解决措施中再做详细讲述。此次回顾只是为下一步高并发的研究做基础准备。

此次笔记略显粗糙,欢迎批评指正,互相学习。

源码码云地址:
https://gitee.com/stefanpy/java

java基础
Web note ad 1