并发-线程

使用线程

进程里可以创建多个线程,线程都有各自的计数器、堆栈和局部变量等属性,并能访问共享内存变量。

使用多线程

  • 更多的处理器核心
  • 更快的响应时间
  • 更好的编程模型

线程优先级

  • 操作系统基本采用时分的形式调度运行的线程,线程分配到若干时间片,当线程的时间片用完了会发生咸亨调度,等待下次分配,线程优先级决定线程需要多或者少分配一些处理器资源的线程属性。

  • setPriority()方法修改优先级,默认是5,优先级范围1到10。

  • 针对频繁阻塞(休眠或io操作)的线程需设置较高优先级,偏重计算的线程设置较低优先级,,确保不会被独占。

  • 线程优先级不能作为程序正确性的依赖

线程状态

  • NEW:初始状态,线程被构建,但还没有调用start()方法
  • RUNNABLE:运行状态,就绪和运行两种状态统称“运行中”
    • 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)的状态
    • 阻塞在java.concurrent包中Lock接口的线程状态缺是等待状态,因为包中lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
  • BLOCKED:阻塞状态,线程阻塞于锁
  • WAITING:等待状态,需要等待其他线程做出特定动作(通知或中断)
  • TIME_WAITING:超时等待状态,不同于waiting,可以在指定的时间自行返回
  • TERMINATED:终止状态,当前线程已经执行完毕

线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换。

  • 线程创建后,调用start()方法开始运行,

  • 线程执行wait()方法,进入等待状态,进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态,

  • 超时等待状态相当于在等待状态的基础上增加了超时限制,超时时间到达时将会返回到运行状态,

  • 线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态,线程在执行runnable的run()方法之后将会进入到终止状态。

Daemon线程

  • 支持型线程,用作程序中后台调度以及支持性工作。

  • 当一个java虚拟机中不存在非Daemon线程的时候,java虚拟机将会退出,可以调用Thread.setDaemon(true)将线程设置为Daemon线程。

  • 在java虚拟机退出时,Daemon线程中的finally块并不一定会执行。构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

启动和终止线程

构造线程

构建线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程组,线程优先级,等

启动线程

调用start()方法启动线程:当前线程同步告知虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

理解中断

线程的标识位属性,表示一个运行中的线程是否被其他线程进行了中断操作。其他线程调用interrupt()方法对其进行中断操作。

线程通过方法isinterrupted进行判断是否被中断,可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。

许多声明抛出InterruptedException方法,这些方法在抛出InterruptedException前会先将该线程的中断标识位清除,再抛出InterruptedException。

过期的suspend()暂停,resume()恢复,stop()停止

不建议使用,方法会带来副作用,暂停和恢复的操作可以使用等待通知机制替代

安全地终止线程

通过标识位或者中断操作的方式能使线程在终止时有机会去清理资源。

countThread.interrupt();

two.cancel();

线程间通信

volatile和synchronized

volatile可以用来修饰字段,告知程序任何对该变量的访问都需要从共享内存中获取,而对他的改变必须同步刷新回共享内存,他能保证所有线程对变量访问的可见性。

synchronized可以修饰方法或以同步块的形式来进行使用,主要确保多个线程在同一个时刻,只能有一个线程处于方法或同步块中,保证了线程对变量访问的可见性和排他性。

  • 同步块实现使用了monitorenter和monitorexit指令
  • 同步方法是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的,本质是对一个对象的监视器进行获取,这个获取过程是排他的,同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
  • 任何一个对象拥有自己的监视器,执行方法的线程必须获取到该对象的监视器才能进入同步块或同步方法,,而没有获取到的将鸡进入阻塞blocked状态
  • 流程:
    • 获取Object的监视器
      • 获取失败,进入同步队列,线程状态变成blocked,如果访问的object前驱(获取锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待/通知机制

  • notify()、notifyAll()、wait()、wait(long)、wait(long,int)
  • 一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

注意

  • 使用wait,notify,notifyAll时需要先调用对象加锁
  • 调用wait后,线程状态由running变成waiting,并将当前线程放置到对象的等待队列
  • notify或notifyAll方法调用后,等待线程依旧不会从wait返回,需要调用notify或notifyAll的线程释放锁后,等待线程才有机会从wait返回
  • notify方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由waiting变成blockd
  • 从wait方法返回的前提是获得了调用对象的锁

等待/通知的经典范式

等待方:消费者

遵循原则

  • 获取对象的锁
  • 如果条件不满足,调用对象的wait方法,被通知后仍要检查条件
  • 条件满足则执行对应的逻辑

通知方:生产者

遵循原则:

  • 获取对象的锁
  • 改变条件
  • 通知所有等待在对象上的线程

管道输入/输出流

用于线程之间的数据传输,传输的媒介为内存。

实现

  • PipedOutputStream(面向字节)
  • PipedInputStream(面向字节)
  • PipedReader(面向字符)
  • PipedWriter(面向字符)

对Piped类型的流,必须先要进行绑定,调用connect方法,如果没有将输入输出流绑定起来,对于该流的访问将会抛出异常

Thread.join()使用

含义:当前线程A等待thread线程终止后才从thread.join返回。

join(long millis) 和join(long millis, int nanos)两个具备超时特性的方法,如果在给定时间里没有终止,name将会从该超时方法中返回

当线程终止时,会调用线程自身的notifyAll方法,会通知所有等待在该线程对象上的线程,步骤:加锁,循环处理逻辑

ThreadLocal使用

线程变量,以ThreadLocal对象为键,任意对象为值的存储结构。结构被附带在线程上,即一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

public class Profiler {
    
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>(){
        protected Long initialValue(){
            return System.currentTimeMillis();
        }
    };
    public static final void begin(){
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
    
    public static final long end(){
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) {
        Profiler.begin();
        TimeUtil.SECONDS.sleep)(1);
        System.out.println("Cost:" + Profiler.end() + " mills");
    }
}

Profiler可以被复用在方法调用耗时统计的功能上。

线程应用实例

等待超时模式

场景:调用一个方法等待一段时间,如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。

假设超时时间段为T,可以推断出在当前时间now+T之后就会超时

定义变量

  • 等待持续时间:REMAINING=T
  • 超时时间:FUTURE=now+T

这时仅需要wait(REMAINING),在wait(REMAINING)返回之后会将执行:REMAINING=FUTURE-now,如果REMAINING小于等于0,表示已经超时,直接退出,否则将继续执行wait(REMAINING)

等待超时模式就是在等待/通知范式基础上增加了超时控制,使该模式相比原有范式更具有灵活性。

一个简单的数据库连接池示例

使用等待超时模式来构造一个简单的数据库连接池,模拟从连接池中获取,使用和释放连接的过程,客户端获取连接的过程被设定为等待超时的模式,即1000毫秒内如果无法获取到可用的链接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过客户端的线程数来模拟无法获取连接的场景。

连接池:初始化连接最大上限,通过一个双向队列维护连接,调用方需要先调用fetchConnection方法指定在多少毫秒内超时获取连接,使用完毕,调用releaseConnection方法将连接池放回线程池

连接池ConnectionPool

public class ConnectionPool {

    private LinkedList<Connection> pool  = new LinkedList<>();
    public ConnectionPool(int initialSize){
        if(initialSize > 0){
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(ConnectionDriver.creatConnection());
            }
        }
    }
    public void releaseConnection(Connection connection){
        if (connection != null){
            synchronized (pool){
                //连接释放后需要进行通知,其他消费者能够感知到连接池中已经归还一个连接
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    public Connection fetchConnection(long mills) throws InterruptedException{
        synchronized (pool){
            if (mills <= 0){
                while (pool.isEmpty()){
                    pool.wait();
                }
                return pool.removeFirst();
            }else {
                long future = System.currentTimeMillis() + mills;
                long remaining = mills;
                while (pool.isEmpty() && remaining > 0) {
                    pool.wait(remaining);
                    remaining = future - System.currentTimeMillis();
                }
                Connection result = null;
                if (!pool.isEmpty()){
                    result = pool.removeFirst()
                }
                return result;
            }
        }
    }
}

动态构造一个connection,该connection代理实现仅仅是在commit方法调用时休眠100毫秒,

public class ConnectionDriver {

    static class ConnectionHandler implements InvocationHandler{
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if(method.getName().equals("commit")){
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return null;
        }
    }
    //创建一个connection的代理,在commit时休眠100毫秒
    public static final Connection createConnection(){
        return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(), new Class<>[]{
                Connection.class}, new ConnectionHandler());
    }
}

模拟客户端ConnectionRunner获取,使用最后释放连接的过程,当它使用时连接将会增加获取到奥连接的数量。

public class ConnectionPoolTest {

    static ConnectionPool pool = new ConnectionPool(10);

    static CountDownLatch start = new CountDownLatch(1);

    static CountDownLatch end;

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new ConnectionRunner(count,got,notGot), "ConnectionRunnerThread");
            thread.start();
        }
        start.countDown();
        end.await();
        System.out.println("total invoke:" + (threadCount * count));
        System.out.println("got connection:" + got);
        System.out.println("not got connection" + notGot);
    }
    static class ConnectionRunner implements Runnable{
        int count;
        AtomicInteger got;
        AtomicInteger notGot;
        public ConnectionRunner(int count, AtomicInteger got, AtomicInteger notGot){
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }
        @Override
        public void run(){
            try {
                start.await();
            }catch (Exception ex){
                
            }
            while (count > 0){
                try {
                    Connection connection = pool.fetchConnection(1000);
                    if (connection != null){
                        try {
                            connection.createStatement();
                            connection.commit();
                        }finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    }else {
                        notGot.incrementAndGet();
                    }
                }catch (Exception ex){
                    
                }finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

线程池技术及其示例

频繁进行线程上下文切换,增加系统负载,线程的创建和消亡都是需要耗费系统资源的,浪费系统资源。

线程池预先创建了若干数量的线程,不能由用户直接对线程的创建进行控制,重复使用固定或较为股多功能数目的线程来完成任务的执行,好处是消除了频繁创建和消亡线程的系统资源的开销,面对过量任务的提交能够平缓的劣化。

实现:

客户端调用execute(Job)方法时,不断向任务列表jobs中添加job,每个工作线程会不断从jobs上取出一个job进行执行,当jobs为空时,工作者线程进入等待状态。

添加一个Job后,对工作队列jobs调用了其notify()方法,而不是notifyAll方法,因为能够确定有工作者线程呗唤醒,这时使用notify()方法将会比notifyAll()方法获得更小开销,避免将等待队列中的线程全部移动到阻塞队列中

线程池的本质就是使用一个线程安全的工作队列连接工作者线程和客户端线程。

  • 客户端线程将任务放入工作队列后便返回
  • 工作者线程不断地从工作队列上取出工作者线程,随着大量的任务被提交,更多工作者线程会被唤醒。

参考:《Java并发编程的艺术》

你可能感兴趣的:(#,java并发编程,java)