java并发基础篇-Thread

/ java并发基础篇 / 没有评论 / 72浏览

前言

线程是任务执行的最小单元

Threa基本内容

线程基本状态

java线程一共有6种基本状态

状态名称说明
NEW线程初试状态,还有start
RUNNING运行状态,也即是操作系统的就绪和运行两种状态
BLOCKED堵塞状态,阻塞于锁
WAITING等待状态,进入等待状态,等待其他线程做出一些特定动作
TIME_WAITING超时等待状态
TERMINATED终止状态

jps可以查看当前jvm进程的lvmid,然后使用jstack id 可以查看id为jvm的线程状态。

线程状态切换基本方式

3AE975C1-4612-4D8F-95C4-E1D6E15D0266.png

线程属性

java线程主要属性

线程方法

public class ThreadJoinTest {
    private static class T implements Runnable {

        private Thread pre;

        public T(Thread pre) {
            this.pre = pre;
        }

        @Override
        public void run() {
            try {
                pre.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread " + Thread.currentThread().getName() + " over");
        }
    }

    public static void main(String[] args) {
        Thread pre = Thread.currentThread();
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new T(pre), "thread " + i);
            threads[i] = thread;
            pre = thread;
        }

        for (Thread thread : threads) {
            thread.start();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            }
        }
    }
}

输出结果:

thread thread 0 over
thread thread 1 over
thread thread 2 over
thread thread 3 over
thread thread 4 over
thread thread 5 over
thread thread 6 over
thread thread 7 over
thread thread 8 over
thread thread 9 over

其他简单方法

方法作用
yieldcpu让步,也就是让出自己cpu的时间,让其他线程或自己执行
sleep休眠,进入TIMED_WATING状态,一般使用TimeUnit.**.sleep
setPriority设置优先级,一般不设置
interrupt设置线程的中断标示为true
isInterrupted当前中断标示位是否为true,不会重制标示为false
静态interrupted当前中断标示位是否为true,会重制标示为false

常见的问题

interrupt中断问题

java线程之间是协程工作方式,所以一个线程,不可以打断另一个线程,但是可以通过interrupt方法,设置线程中断状态,然后自行处理是否退出或者做其他工作。

group线程组

group线程组不是用来管理线程的,他只是组织一些线程在一个组织里,会使用相同的属性而已,默认是main所在的线程组,名字也是main


thread和object.wait,notify,notifyAll

基本使用

object的wait方法是堵塞当前线程的常用方法,主要是等待其他县城完成某项前置条件后,唤醒堵塞的线程,也是java内置的等待/通知机制。

线程池方案

public class ThreadPool {


    private int poolSize = 10;

    private final List<Worker> workers = new ArrayList<>(10);

    private final LinkedList<Runnable> linkedBlockingQueue = new LinkedList<>();


    public ThreadPool() {
        initWorkThread();
    }


    public void addJob(Runnable runnable) {
        synchronized (linkedBlockingQueue) {
            linkedBlockingQueue.add(runnable);
            linkedBlockingQueue.notify();
        }
    }


    public void close() {
        close = true;
        synchronized (linkedBlockingQueue) {
            linkedBlockingQueue.notifyAll();
        }
    }

    private void initWorkThread() {

        synchronized (workers) {
            for (int i = 0; i < poolSize; i++) {
                Worker worker = new Worker("threadPool - " + i);
                workers.add(worker);
                worker.start();
            }
        }
    }

    private boolean close = false;


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

        @Override
        public void run() {
            while (true) {
                if (close) {
                    System.out.println("thread " + Thread.currentThread().getName() + " close");
                    return;
                }
                Runnable runnable;
                synchronized (linkedBlockingQueue) {
                    if (linkedBlockingQueue.size() == 0) {
                        try {
                            linkedBlockingQueue.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                }
                if (!linkedBlockingQueue.isEmpty()) {
                    runnable = linkedBlockingQueue.removeFirst();
                    try {
                        runnable.run();
                    } catch (Exception e) {

                    }
                }

            }
        }

    }


    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool();
        for (int i = 0; i < 20; i++) {
            final int number = i;
            threadPool.addJob(() -> {
                System.out.println("任务" + number + " sleep 10s");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                }
                System.out.println("任务" + number + " over");
            });
        }
    }

}

输出结果:

任务2 sleep 10s
任务5 sleep 10s
任务4 sleep 10s
任务1 sleep 10s
任务3 sleep 10s
任务6 sleep 10s
任务0 sleep 10s
任务7 sleep 10s
任务8 sleep 10s
任务9 sleep 10s
任务8 over
任务2 over
任务4 over
任务12 sleep 10s
任务6 over
任务3 over
任务7 over
任务0 over
任务16 sleep 10s
任务15 sleep 10s
任务14 sleep 10s
任务13 sleep 10s
任务11 sleep 10s
任务1 over
任务17 sleep 10s
任务5 over
任务10 sleep 10s
任务9 over
任务18 sleep 10s
任务19 sleep 10s

上面的代码是一个简单的线程池,主要思路就是对任务容器(LinkedList)加锁,并执行等待,一旦添加新的任务时,唤醒某个线程池的线程,并执行对应的任务。

object的wait和notify为什么需要获取对象的锁

wait和notify是典型的生产者和消费者的设计模式

从语言上分析,在生产者和消费者的设计模型里,消费者在没有获取到资源时需要等待,生产者在生产出资源后需要通知,那么在典型的代码临界区中,生产者和消费者的代码需要保证同步。我们在反过来看,如果不加锁会出现什么情况。

代码如下

public class Fake {
        public final Queue<String> buffer = new LinkedList<>();
        public String take() throw InterruptedException{
            while (buffer.isEmpty()) {         一

                buffer.wait();                 二
            }
            return buffer.poll();
        }

        public void put(String string) {
            buffer.add(string);                三
            
            buffer.notifyAll();                四
        }
    }

代码标记了4个步骤,那么在多线程无锁的情况,可能发生的顺序是1,3,4,2。

执行的结果是:

  1. 消费者A获取资源,发现容器是空的,此时消费者A的线程释放cpu。
  2. 生产者B获取CPU,生产资源,加入容器中(此时容器非空了)
  3. 生产者B通知消费者A,容器有资源了,但是由于上面消费者A,没有进入等待状态,所以无法接收到此通知。
  4. 消费者A此时进入等待,如果容器只能添加一个资源,那么消费者A无法唤醒了

可以看到,消费者A丢失了一次关键的通知,造成了无限等待的情况(除非消费者B再次生产一个资源,才可以唤醒),所以核心点在于,要保证条件判断和更改的原子性

上面的例子并不是典型的生产者-消费者模式,需要在生产者缓存临界值进行等待,防止生产过多。

object的notify和notifyAll区别

首先看下,jdk的api的注视:

 /**
     * Wakes up a single thread that is waiting on this object's
     * monitor. If any threads are waiting on this object, one of them
     * is chosen to be awakened. The choice is arbitrary and occurs at
     * the discretion of the implementation. A thread waits on an object's
     * monitor by calling one of the {@code wait} methods.
     * <p>
     * The awakened thread will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened thread will
     * compete in the usual manner with any other threads that might be
     * actively competing to synchronize on this object; for example, the
     * awakened thread enjoys no reliable privilege or disadvantage in being
     * the next thread to lock this object.
     * ...
     * Only one thread at a time can own an object's monitor.
     */
     /**
     * 翻译大概意思:唤醒一个等待对象监控器的线程,如果有很多线程在等待中,则选择唤醒其中一个。这个选择是随意的由实现着决定
     * 线程通过调用wait方法进行等待对象监控器。
     * 被唤醒的线程直到当前线程释放对象锁后才可以执行。唤醒的线程将以通常的方式与可能正在竞争同步此对象的任何其他线程竞争。例如:
     * 被唤醒的线程没有任何可靠的特权获取到对象的锁。
     * ...
     * 在同一时间,只能有一个线程获取到对象的监控器
     */
    public final native void notify();

    /**
     * Wakes up all threads that are waiting on this object's monitor. A
     * thread waits on an object's monitor by calling one of the
     * {@code wait} methods.
     * <p>
     * The awakened threads will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened threads
     * will compete in the usual manner with any other threads that might
     * be actively competing to synchronize on this object; for example,
     * the awakened threads enjoy no reliable privilege or disadvantage in
     * being the next thread to lock this object.
     * <p>
     * 
     * ...
     * 翻译大概意思:
     *      唤醒所有的等待对象监控器的线程,线程通过调用wait方法进行等待对象监控器。
     *      被唤醒的线程们直到当前线程释放对象锁后才可以执行,唤醒的线程们将以通常的方
     *      式与可能主动竞争同步此对象的任何其他线程竞争,例如:被唤醒的线程没有任何可
     *      靠的特权获取到对象的锁。
     */
    public final native void notifyAll();

简单点说,就是一个唤醒所有线程一个唤醒一个线程,但是在同一时间只能有一个线程执行

通过一个具体的例子分析下:

private final Object lockObject = new Object();

    @Test
    public void testNotify() {
        IntStream.range(0, 10).forEach((i) -> {
            Thread thread = new Thread(() -> {
                System.out.println("thread " + i + " wait ...");
                synchronized (lockObject) {
                    try {
                        lockObject.wait();
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println("thread " + i + " execute over");
            });
            thread.start();
        });
        System.out.println(" main thread sleep 5 seconds");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lockObject) {
            System.out.println(" lockObject notify...");
            lockObject.notify();
        }
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lockObject) {
            System.out.println(" main thread sleep more 5 seconds");
            lockObject.notifyAll();
        }
    }
    
输出结果:
thread 0 wait ...
thread 1 wait ...
thread 2 wait ...
thread 3 wait ...
thread 4 wait ...
thread 5 wait ...
thread 6 wait ...
thread 7 wait ...
 main thread sleep 5 seconds
thread 8 wait ...
thread 9 wait ...
 lockObject notify...
 main thread sleep more 5 seconds
thread 0 execute over
 lockObject notifyAll...
thread 9 execute over
thread 6 execute over
thread 3 execute over
thread 7 execute over
thread 8 execute over
thread 1 execute over
thread 2 execute over
thread 4 execute over
thread 5 execute over

可以看到,notify只会唤醒一个线程,其他线程仍为等待状态,notify会唤醒所有的线程,其他线程变为堵塞状态竞争锁,同一时间只能有一个执行。

所以在选择notify和notifyAll方法时,需要注意到,如果wait线程执行的内容不一样,那么当我们完成其中一个线程等待的条件时,不能准确的唤醒这个线程,需要使用notifyAll唤醒所有线程,让线程自行处理。如果所有的此案成执行的内容一致的话,我们就可以使用notify方法,避免cpu资源浪费。

monitor是对象的监控器,java中每个对象都会对应一个monitor(ObjectMonitor)对象,java对象头中存储该对象指针,由虚拟机HotSpot实现。 。ObjectMonitor有个_owner变量,指向当前获取monitor的线程。还有两个队列_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后,_owner指向当前线程,若此时线程调用 wait() 方法,将释放当前持有的monitor并进入 WaitSet集合中等待被唤醒

其他

jvm退出钩子函数

在虚拟机退出的时候可以添加一些钩子函数,用来执行某些操作。

Runtime.getRuntime().addShutdownHook(Thread)

sleep和wait区别

sleep是线程对象的方法,可以打断,不需要获取对象锁

wait是object方法,可以打断,需要获取对象锁。

结束

本文都是些线程的基本知识,可能会有些缺漏,后面会不定期增加缺失的内容