两篇文章了解进程与线程( 进阶篇)

一、守护线程与yield

1. 守护线程(Daemon):

   public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }
 public final boolean isDaemon() {
        return daemon;
    }

setDaemon()方法是将线程标记为daemon线程或用户线程。当运行的唯一线程都是守护线程时,JVM将退出。

isDaemon()方法是测试当前线程是否是守护线程。

public class ThreadDemo {
    public static void main(String[] args) {

        Thread thread = new Thread(new MyRunnableThread());

        thread.setDaemon(true);// 把线程设置为守护线程。当进程中没有用户线程是,JVM将退出,设置需要在启动之前设置
        thread.start();
        for (int i = 0; i < 50 ; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程(用户线程)当前执行:"+i);

        }

    }
}
class MyRunnableThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 50 ; i++) {

            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyRunnableThread当前执行:"+i);
        }
    }
}

案例通过守护线程和用户线程(mainThread)做比较,注意,使用守护线程时要在start()之前,等待CPU调度之前设置为守护线程。 否则处于Alive()状态,会抛出异常。异常解释看上篇文章:两篇文章了解进程与线程( 基础篇)

可以看出,当主线程结束时,JVM退出,守护线程也就结束了。那么这个有什么用呢?举个例子,当我们的项目涉及用户下载时,当用户退出应用,主线程结束,如果非守护线程,下载继续执行,所以可以将下载功能依托于守护线程,当用户退出时退出,在Android开发中较常用。 但不要涉及到数据存储,否则用户线程结束时候,数据可能没来得及保存好。

2. yield:

   public static native void yield();

调用本地方法(C/C++写的功能),让一次。

准确来说,我觉得这个方法没啥用。。。

public class ThreadDemo {
    public static void main(String[] args) {

        Thread thread = new Thread(new MyRunnableThread());

        thread.start();
        for (int i = 0; i < 50 ; i++) {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程(用户线程)当前执行:"+i);

        }

    }
}
class MyRunnableThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 50 ; i++) {

            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 6) Thread.yield();
            System.out.println("MyRunnableThread当前执行:"+i);
        }
    }
}

就让一次,抢CPU时间片时,当i == 6时候,让一次调度,并执行其他线程。

二、线程同步:

 1. 多线程共享数据:

在多线程的操作中,多个线程可能处理同一个数据资源,这就是多线程中的共享数据。

以售票为模拟,假设有十张票,两个窗口售卖。常规开两个线程同时处理会出现数据为假的问题,比如线程1已经卖了就剩3张了,可能线程2还有4张。

错误示例:

public class ThreadDemo {
    public static void main(String[] args) {

        Thread thread1 = new Thread(new MyRunnableThread());// 第一个售票窗口
        Thread thread2 = new Thread(new MyRunnableThread());// 第二个售票窗口
        thread1.start();
        thread2.start();


    }
}

class MyRunnableThread implements Runnable {
    private int ticket = 10;// 售票

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            ticket--;
            if (ticket > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("还剩" + ticket + "张票。");
            }
        }
    }
}

当然这是也是数据库事务的问题,只是用案例做个线程示例。可以看到票会被多卖,造成线程不安全的问题。

如何来做数据共享?我们引入线程同步的概念。解决数据共享问题,必须使用同步,所谓同步就是指多个线程在同一时间段内只能由一个线程能执行指定代码,其他线程要等执行线程结束后才可以继续执行。

线程同步有三种方法:

1)synchronized(同步的对象){同步的方法}

2)public synchronized void method(){要同步的操作}

3)Lock 锁

public class ThreadDemo {
    public static void main(String[] args) {

        MyRunnableThread myRunnableThread = new MyRunnableThread();
        Thread thread1 = new Thread(myRunnableThread);// 第一个售票窗口
        Thread thread2 = new Thread(myRunnableThread);// 第二个售票窗口
        thread1.start();
        thread2.start();


    }
}

class MyRunnableThread implements Runnable {
    private int ticket = 10;// 售票

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {

            if (ticket > 0) {
                synchronized (this){// 当前对象,进行统一锁
                    ticket--;
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("还剩" + ticket + "张票。");
                }

            }
        }
    }
}

使用synchronized()进行同步锁,参数为对象,但需要两个自定义Thread使用同一对象,否则锁无效。如果非统一对象,也没意义对吧。

 

操作成功。 这里可以看出基础篇中的sleep方法不会丢失所有权,这里也就可以看得出,两个线程同步执行时,虽然休眠线程存在切换,但对象和数据没变。

可以使用同步方法进行处理,返回为synchronized。默认使用的对象是this。

public class ThreadDemo {
    public static void main(String[] args) {

        MyRunnableThread myRunnableThread = new MyRunnableThread();
        Thread thread1 = new Thread(myRunnableThread);// 第一个售票窗口
        Thread thread2 = new Thread(myRunnableThread);// 第二个售票窗口
        thread1.start();
        thread2.start();


    }
}

class MyRunnableThread implements Runnable {
    private int ticket = 10;// 售票

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {


            method();
        }
    }

    private synchronized void method() {
        if (ticket > 0) {
            ticket--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("还剩" + ticket + "张票。");
        }
    }
}

使用Lock锁进行处理

public class ThreadDemo {
    public static void main(String[] args) {

        MyRunnableThread myRunnableThread = new MyRunnableThread();
        Thread thread1 = new Thread(myRunnableThread);// 第一个售票窗口
        Thread thread2 = new Thread(myRunnableThread);// 第二个售票窗口
        thread1.start();
        thread2.start();


    }
}

class MyRunnableThread implements Runnable {
    private int ticket = 10;// 售票

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            method();
        }
    }

    ReentrantLock lock = new ReentrantLock();// 互斥锁
    // lock方法进行同步
    private  void method() {
        lock.lock();// 锁住
        if (ticket > 0) {
            ticket--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                lock.unlock();// 释放
            }
            System.out.println("还剩" + ticket + "张票。");
        }
    }
}

三种方法:

同步代码块,同步方法,互斥锁,可以看得出互斥锁更为灵活。

2. 同步准则:

当编写synchronized块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面有很大的帮助:

1) 使用代码块保持简洁,把不随线程变化的预处理和后处理移除代码块。

2) 不要阻塞。

3) 在持有锁的时候,不要对其他对象调用方法。

多线程共享数据会有安全问题,使用同步可以解决安全问题,但会牺牲性能,所以要满足上述的准则。

3. 线程死锁:

过多的同步有可能会出现死锁,死锁的操作一般是在程序运行的适航才会出现。多线程中要进行资源共享就要同步,同步过多就会出现死锁问题。

三、生产者与消费者:

多线程的开发中有一个经典的操作案例,那就是生产者-消费者,生产者不断生产产品,消费者不断取走产品。

例如:在饭店里有一个厨师一个服务员,这个服务员必须等待厨师准备好食物,当厨师准备好后,会通知服务员,服务员取走菜,一个协作的示例。

public class ProducerConsumerDemo {
    public static void main(String[] args) {

        Food food = new Food();
        Producer p = new Producer(food);
        Consumers c = new Consumers(food);
        Thread tp = new Thread(p);
        Thread cp = new Thread(c);
        tp.start();
        cp.start();
    }

}

/**
 * 生产者
 */
class Producer implements Runnable {
    private Food food;

    public Producer(Food food) {
        this.food = food;
    }

    @Override
    public void run() {

        for (int i = 0; i < 20; i++) {// 模拟生产20。奇数一种菜,偶数一种菜分菜
            if (i % 2 == 0) {
                food.set("锅巴", "农家锅巴");
            } else {
                food.set("臭鳜鱼", "农家臭鳜鱼");
            }
        }
    }
}

/**
 * 消费者
 */
class Consumers implements Runnable {
    private Food food;

    public Consumers(Food food) {
        this.food = food;
    }

    @Override
    public void run() {

        for (int i = 0; i < 20; i++) {//消费20
            food.get();
        }
    }
}

// 食物类
class Food {

    private boolean flag = true; //中间变量,true表示可以生产,false表示消费
    private String name;

    private String desc;

    public Food(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    public Food() {
    }

    /**
     * 生产产品
     *
     * @param name
     * @param desc
     */
    public synchronized void set(String name, String desc) {
        if (!flag) { //不能生产

            try {
                this.wait();// 进入等待状态,释放监视器的所有权(对象锁)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.setName(name);
        try {
            Thread.sleep(500);// 模拟生产时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setDesc(desc);
        flag = false;
        this.notify();//唤醒其中一个线程共两个
    }

    /**
     * 消费产品
     */
    public synchronized void get() {
        if (flag) { //不能消费

            try {
                this.wait();// 进入等待状态,释放监视器的所有权(对象锁)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(500);// 模拟取得时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.getName() + "->" + this.getDesc());
        flag = true;
        this.notify();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Food{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

生产者与消费者协同工作。其中 我们用了wait()方法,同样是等待,sleep()和wait()方法有什么区别。(面试题

sleep :让线程进入休眠状态,让出CPU的时间片,但不释放对象监视器的所有权(对象锁)。

wait:让出线程进入等待状态,让出CPU的时间片,并释放对象监视器的所有权(对象锁),等待其他线程通过notify()进行唤醒。

notify():唤醒所有等待线程中的随机一个线程。

notifyAll():唤醒所有等待的线程。

四、线程的生命周期:

一张图可以看出线程的生命周期,不过多解释了。 

五、线程池:

线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲的队列中,然后对这些资源进行复用,减少频繁的创建和销毁对象。

JDK1.5以上版本提供了现成的线程池。Java中线程池的顶级接口时Excutor,是一个执行线程的工具。线程池接口时ExecutorService。

public interface ExecutorService extends Executor{}

Executor 接口:执行已提交的Runnable任务的对象。

ExecutorService接口:继承Executor,Executor提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成的Future的方法。

Executors类:定义了上述接口的工厂和使用方法。

四种方法创建线程池:

1)newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执。

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // 创建线程池(4中方式)
        ExecutorService es = Executors.newSingleThreadExecutor();// 创建固定线程池
        es.execute(new MyRunable());
        es.execute(new MyRunable());
        es.shutdown();
    }
}
class MyRunable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i++) {
            System.out.println("任务"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2)newFixedThreadPool:

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // 创建线程池(4中方式)
        ExecutorService es = Executors.newFixedThreadPool(2);// 创建固定大小线程池
        es.execute(new MyRunable());
        es.execute(new MyRunable());
        es.shutdown();
    }
}
class MyRunable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3)newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // 创建线程池(4中方式)
        ExecutorService es = Executors.newCachedThreadPool();// 创建不固定大小线程池,可缓存
        es.execute(new MyRunable());
        es.execute(new MyRunable());
        es.shutdown();
    }
}
class MyRunable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4)newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // 创建线程池(4中方式)
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);// 创建不固定大小线程池,指定命令和延时执行的线程池
        ses.execute(new MyRunable());
        ses.execute(new MyRunable());
        ses.schedule(new MyRunable(),3000, TimeUnit.MICROSECONDS);
        ses.shutdown();
    }
}
class MyRunable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

总结:

 通过两篇文章让大家了解一下线程的概念,从源码出发去查看别人的设计思路,学习JAVA不能避免的就是查看方法的设计源码。

发布了73 篇原创文章 · 获赞 152 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/qq_37857921/article/details/105238064