Java基础 - 多线程

2019-04-15  本文已影响0人  ADMAS

多线程

并行和并发

这里的时间都是微观上的概念

当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度,这种情况下,线程是一个接一个执行的。

进程和线程

进程中的多个线程是并发执行的,从微观上来讲,线程执行是有先后顺序的,那么哪个线程执行完全取决于CPU调度器,程序员是控制不了的。

我们可以把多线程并发性看作是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。

Java程序的进程里至少包含主线程和垃圾回收线程(后台线程)。

多线程的优势

多线程作为一种多任务、并发的工作方式,当然有其存在优势:

这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,可以避免这种尴尬的情况

操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上

一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

Java操作进程

Runtime:

多线程的创建

继承Thread类:

  1. 自定义类继承于Thread类,那么该自定义类就是线程类;
  1. 覆写run方法,将线程运行的代码存放在run中;
  1. 创建自定义类的对象,即线程对象;
  1. 调用线程对象的start方法,启动线程。

实现Runnable接口:

  1. 自定义类实现Runnable接口;
  1. 覆写run方法,线程运行的代码存放在run中;
  1. 通过Thread类创建线程对象,并将实现了Runnable接口的实现类对象作为参数传递给Thread类的构造器。
  1. Thread类对象调用start方法,启动线程。

继承和实现的对比:

线程并发的安全问题

多个线程访问共享区域(堆),产生了数据不安全的问题(不同步).

场景:

当线程A只执行了变量值的修改,还未打印变量的值,此时线程的执行权被其他的线程B所抢走,别的线程又修改了变量的值,并打印了变量值,当原来的线程A抢回执行权后变量的值已经被别的线程B所修改,线程A打印的值就与线程B打印的值相同, 所以就有了数据重复的问题

解决安全问题的方法:

懒汉式的单例模式:

private static $CLASS_NAME$ instance ;

private $CLASS_NAME$($param1$){
$init$
}

public static $CLASS_NAME$ getInstance($param1$){
    //如果有多个线程进来后,1个线程进入后,其他线程被堵在外面,效率比较差,增加一个null判断.
    if (instance == null) {
        synchronized ($CLASS_NAME$.class){
            if (instance == null) {
                instance = new $CLASS_NAME$($param2$) ;
             }
        }
    }
    return instance ;
}

生产消费者案例

由生产者(producer)往共享空间存放数据,再由消费者(consumer )取出数据

共享数据
public void add(String name, String taste) {
    try {
        this.name = name;
        System.out.println("做好了菜:"+name);
        Thread.sleep(5);
        this.taste = taste;
        System.out.println("调味后生产了一道菜:" + name + "   口味:" + taste);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

public void take() {
    try {
        Thread.sleep(5);
        System.out.println("消费了一道菜:" + name + "   口味:" + taste);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

生产消费者线程

public  void main(){
    //共享的数据区域
    ShareData data = new ShareData();
    //生产者线程
    new Thread(new Producer(data)).start();
    //消费者线程
    new Thread(new Consumer(data)).start();
}

public class Producer implements Runnable{
    private ShareData mShareData;

    public Producer(ShareData shareData) {
        mShareData = shareData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                mShareData.add("麻辣小龙虾","麻辣");
            }else{
                mShareData.add("清蒸排骨","清淡");
            }
        }
    }
}

public class Consumer implements Runnable{
    private ShareData mShareData;

    public Consumer(ShareData shareData) {
        mShareData = shareData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            mShareData.take();
        }
    }
}

出现问题:

 做好了菜:麻辣小龙虾
 调味后生产了一道菜:麻辣小龙虾   口味:麻辣
 做好了菜:清蒸排骨
 调味后生产了一道菜:清蒸排骨   口味:清淡
 做好了菜:麻辣小龙虾
 消费了一道菜:麻辣小龙虾   口味:清淡
 调味后生产了一道菜:麻辣小龙虾   口味:麻辣
 做好了菜:清蒸排骨
 消费了一道菜:清蒸排骨   口味:麻辣
 调味后生产了一道菜:清蒸排骨   口味:清淡
 做好了菜:麻辣小龙虾
 消费了一道菜:麻辣小龙虾   口味:清淡
 调味后生产了一道菜:麻辣小龙虾   口味:麻辣
 做好了菜:清蒸排骨
 消费了一道菜:清蒸排骨   口味:麻辣
 调味后生产了一道菜:清蒸排骨   口味:清淡
 做好了菜:麻辣小龙虾
 消费了一道菜:麻辣小龙虾   口味:清淡
 调味后生产了一道菜:麻辣小龙虾   口味:麻辣
 做好了菜:清蒸排骨
 消费了一道菜:清蒸排骨   口味:麻辣
 调味后生产了一道菜:清蒸排骨   口味:清淡
  1. 先消费,后生产

    生产者已经生产好后,正准备打印的时候,CPU调度了消费者的线程,生产者的线程停下来了,先由消费者消费,自然出现了先消费的现象

    解决方案:

      必须先等生产者生产好后,再由消费者消费
    
  2. 多消费,多生产

    生产的线程在第一执行时被抢了,先出现先消费的情况后,下一次执行时,比较顺利就会看到生次的情况,同样的道理,对于消费者来讲,就会出现消费两次的情况

    解决方案:

      必须先等生产者生产好后,再由消费者消费
    
  3. 菜和口味不符

    在生产者生产的时候,只做了菜名,没有上调料的时候,线程的执行被消费者抢了,
    消费者访问到的是新做的菜和上次留下的调料,出现了口味的错乱的问题

    解决方案:

      必须先等生产者生产好后,再由消费者消费,需要加上同步
    

解决方案一:

1.要保证同步

使用同步的方法

2.要保证生产者先执行

先设置一个标志判断是否有东西,没有就执行生产者,否则执行消费

3.要保证线程中的内容执行完毕,另一个线程才能执行

使用等待唤醒机制(Object)

Object的等待唤醒:

wait(); //使当前线程进入等待, 必须由当前线程的同步监听对象调用

notifyAll();//唤醒在同步对象上等待的所有线程,必须由当前线程的同步监听对象调用

改良后的代码:

public class ShareData {
private String name;
private String taste;
private boolean hasData;
//同步方法
public synchronized void add(String name, String taste) {
    try {
        if (hasData){
            //存在数据,生产者线程进入等待
            this.wait();
        }
        this.name = name;
        System.out.println("做好了菜:"+name);
        Thread.sleep(5);
        this.taste = taste;
        System.out.println("调味后生产了一道菜:" + name + "   口味:" + taste);
        //生产完毕,改变标记,唤醒消费者线程
        hasData = true;
        this.notifyAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

//同步方法
public synchronized void take() {
    try {
        if (!hasData){
            //无数据,消费者线程等待
            this.wait();
        }
        Thread.sleep(5);
        System.out.println("消费了一道菜:" + name + "   口味:" + taste);
        //消费完毕,改变标记,唤醒生产者线程
        hasData = false;
        this.notifyAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}
}

解决方案二:

使用锁的方式

Condition

示例代码:

public class ShareDataForLock {
private String name;
private String taste;
private boolean hasData;
//必须先创建锁对象
private Lock mLock;
//锁的同步监听对象
private Condition mCondition;

public ShareDataForLock() {
    mLock = new ReentrantLock();
    mCondition = mLock.newCondition();
}

public  void add(String name, String taste) {
    mLock.lock();//加锁
    try {
        if (hasData){
            //存在数据,生产者线程进入等待
            mCondition.await();
        }
        this.name = name;
        System.out.println("做好了菜:"+name);
        Thread.sleep(5);
        this.taste = taste;
        System.out.println("调味后生产了一道菜:" + name + "   口味:" + taste);
        //生产完毕,改变标记,唤醒消费者线程
        hasData = true;
        mCondition.signalAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
        mLock.unlock();//释放锁
    }

}

//同步方法
public synchronized void take() {
    mLock.lock();//加锁
    try {
        if (!hasData){
            //无数据,消费者线程等待
            mCondition.await();
        }
        Thread.sleep(5);
        System.out.println("消费了一道菜:" + name + "   口味:" + taste);
        //消费完毕,改变标记,唤醒生产者线程
        hasData = false;
        mCondition.signalAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
        mLock.unlock();//释放锁
    }

}}

睡眠和等待的区别

Thread的sleep方法和Object的wait方法

1:他们都能让线程暂停执行

2:sleep方法在暂停执行的过程中是不会失去同步监听对象

wait方法在暂停执行的过程中会失去同步的监听对象,醒来后会重新拿到同步监听对象,再执行代码

死锁

所谓死锁就是锁死了,由于同步使用不当(一般是由于同步嵌套同步),产生的一种线程代码不执行的现象,JVM不会关闭,死锁一旦产生,无法挽救,只能事先避免,也称为线程的阻塞状态

共享的同步监听对象得不到释放,线程在互相等待资源释放,造成死锁的现象

示例代码:

    final String a = "333";
    final String b = "555";
    new Thread(){
        @Override
        public void run() {
           while (true){
                synchronized (a){
                    System.out.println("thread1:11");
                    synchronized (b){
                        System.out.println("thread1:22");
                    }
                }
           }
        }
    }.start();

    new Thread(){
        @Override
        public void run() {
            while (true){
                synchronized (b){
                    System.out.println("thread2:1111");
                    synchronized (a){
                        System.out.println("thread2:2222");
                    }
                }
            }
        }
    }.start();

执行结果:
I/System.out: thread2:1111
I/System.out: thread2:2222
I/System.out: thread2:1111
I/System.out: thread1:11  //此时a对象被t1占用,t2等待t1释放a,t1等待t2释放b,死锁

要想不发生死锁,建议使用Lock对象来完成同步

线程生命周期

联合线程

线程的join()方法表示一个线程等待另一个线程完成后才执行。

有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。

后台线程

在后台运行,其目的是为其他线程提供服务,也称为"守护线程"。JVM的垃圾回收器就是典型的后台线程。

特点:

若所有的前台线程都死亡,后台线程自动死亡。

thread.isDaemon() ---> 测试后台线程的方法
thread.setDaemon(true) ---> 设置后台线程的方法  线程创建默认是前台线程,
该方法必须在start()前调用,否则出现IllegalThreadStateException异常.

线程优先级

线程局部

ThreadLocal和线程同步机制

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

线程池

Executor

定时器

Timer:

一种工具,用其安排在后台线程中执行任务,可安排执行一次,也可以定期重复执行.

Timer timer =new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("1000毫秒后执行了任务");
        }
    },1000);
上一篇下一篇

猜你喜欢

热点阅读