Java 多线程

Java 多线程

线程和进程的区别

  • 线程和进程的本质:由CPU进行调度的并发式执行任务,多个任务被快速轮换执行,使得宏观上具有多个线程或者进程同时执行的效果。

  • 进程:在操作系统来说,一个运行的程序或者说一个动态的指令集合通常对应一个进程Process,它是系统进行资源分配和调度的一个独立单位,也是拥有系统资源的基本单位。进程是系统中独立存在的实体,它可以拥有自己独立的资源,拥有自己私有的地址空间,进程之间不能直接访问其他进程的地址空间。
  • 线程:线程是CPU调度的基本单位,也就是说在一个进程中可以有多个并发程序执行流,线程拓展了进程的概念,使得任务的执行得到更加的细分,所以Thread有时候也被称为Lightweight Process。线程是进程的执行单元,但是线程不是分配系统资源的单位,它们共享所在进程的资源,包括共享内存,公有数据,全局变量,进程文件描述符,进程处理器,进程代码段,进程用户ID等等。
  • 线程独立拥有自己的线程ID,堆栈,程序计数器,局部变量,寄存器组值,优先级,信号屏蔽码,错误返回码等等,线程是独立运行的,其执行是抢占式的。线程共享进程资源,线程之间的通信要进程之间的通信来得容易得多。此外,线程的创建和销毁的开销也远远小于进程的系统开销。

线程和线程池

  • 线程池:虽然线程的创建销毁的开销相对较小,但是频繁得创建和销毁也会消耗有限的资源,从而带来性能上的浪费,也不够高效。因此线程池的出,现就是为了解决这一问题,即在初始状态创建并维护一定数量的空闲线程,当有需要执行的任务,就交付给线程中的一个线程,任务执行结束后,该线程也不会死亡,而是回到线程池中重新变为空闲状态。
  • 线程池的好处:减少线程频繁创建销毁的资源开销,同时能够有效控制系统中并发线程的数量,防止系统性能的剧烈下降。

线程创建/启动的三种方法

  • 继承Thread类创建多线程,此时每次创建的Thread对象并不能共享线程类的实例变量,也就是下面程序中的i

    public class FirstThread extends Thread{
    private int i;
    @Override
    public void run() {
        for(i=0;i<10;i++) 
            System.out.println(getName()); // 继承自Thread 
    }
    
    public static void main(String[] args) {    
        new FirstThread().start();
        new FirstThread().start(); // 注意启动线程需要用Start
    }
    }
  • 实现Runnable接口创建线程类,Runnable接口是一个函数式接口(可以使用Lambda表达式),通常做法是重写接口中的run方法,此时方法体即为线程执行体,使用Runnable接口实现类的实例作为Thread的target来创建Thread对象,此时因为使用一个共同的target线程执行体,多个线程可以共享一个实例变量。

    public class SecondThread implements Runnable{
    private int i;
    @Override
    public void run() {
        for(;i<10;i++) {
            System.out.println(Thread.currentThread().getName() + " "+ i);
        }
    }
    
    public static void main(String[] args) {
        SecondThread targetRunnable = new SecondThread();
        new Thread(targetRunnable,"线程1").start();
        new Thread(targetRunnable).start();
    }
    }
  • 使用Callable和Future创建线程:Callable类似于Runnable,提供一个Call()方法作为线程执行体,并且可以有返回值,以及抛出异常,那么我们如何拿到返回值,java提供了future接口,在接口里定义了一些公共方法来控制关联它的Callable任务,然后java还贴心的给了FutureTask类,该类实现了Future接口和Runnable接口,所以FutureTask的实例就可以作为Thread的Target,所以通常做法是创建Callable接口实现类,并对该实现类的实例使用FutureTask来包装。

    public class ThridThread {
    public static void main(String[] args) {        
        // lambda 表达式 + functionInterface 类型转换
        // Callbable: 有返回值
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            int i =0;
            for(;i<100;i++) {
                System.out.println(Thread.currentThread().getName() + " "+ i);
            }
            return i;
    }); 
        new Thread(task,"有返回值的线程").start();
        try {
            System.out.println("子线程的返回值"+task.get());
        }catch(Exception e) {
            e.printStackTrace();
        }
     }
    }

线程的生命周期

  • 线程的生命周期包括:新建New,就绪Runnable,运行Running,阻塞Blocked,和死亡Dead 5种状态。
  • 新建和就绪状态:程序使用new关键字之后,该线程就处于新建状态,jvm为其分配内存,并初始化成员变量。程序调用start() 方法之后,该线程就处于就绪状态,jvm为其创建方法调用栈和PC计数器。
  • 运行和阻塞状态:如果就绪状态的线程获得了CPU,那么程序就处于运行状态。
    当发生如下情况时,线程将进入阻塞状态。
  1. 线程调用sleep()方法,主动放弃所占有的CPU资源。
  2. 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  3. 线程试图获得一个同步监视器,但是该同步监视器被其他线程所持有
  4. 线程等待某个通知notify,notify通常与wait配合使用
  5. 线程调用suspend(),挂起,该方法容易造成死锁,不建议使用。

当发生如下情况时,线程进入就绪状态,但是线程什么时候进入运行状态,需要根据系统调度来决定。

  1. sleep()方法的线程经过了指定的sleep的时间
  2. 阻塞式IO方法返回值
  3. 成功获得了同步监视器
  4. 线程获得了其他线程发出的通知,被唤醒
  5. 挂起的线程调用了Resume()方法恢复。

屏幕快照 2017-12-01 上午11.58.30.png

屏幕快照 2017-12-01 下午2.19.31.png

  • 线程死亡:线程执行体执行结束,以及抛出一个未捕获的Exception或Error,或者直接调用stop()方法结束该线程。可以通过线程对象的isAlive()方法,来判断线程对象的状态(新建或者死亡都会返回false)

注意:抢占式策略系统:系统会给每个执行的线程一个小的时间段来处理任务,当该时间段用完之后,系统会剥夺该线程所占用的资源,让其他线程获得执行的机会。在系统调度时,还会考虑到线程的优先级问题。

线程控制

  • join()-线程:让一个线程等待另一个线程,当在某个线程执行流中调用其他线程的join()方法,该线程将被阻塞,知道join线程执行完毕为止。

  • 后台-线程:后台线程又称为Daemon Thread,守护线程,JVM的垃圾回收线程就是典型的后台线程。特征是:如果所有前台线程都死亡,那么后台线程自动死亡。调用Thread对象的setDaemon(true)可以将指定线程设置为后台线程,注意需要在Start()之前调用,主线程默认为前台线程,前台线程创建的子线程默认为前台线程,后台线程创建的子线程默认为后台线程
  • sleep()-线程:sleep(ms)是Thread类的静态方法,让当前线程暂停millis毫秒,并进入阻塞状态,睡眠状态的线程不会释放同步监视器,在此期间该线程不会获得执行的机会。注意使用sleep方法时需要捕捉InterruptedException或者抛出该异常

    public class SleepThread {
    public static void main(String[] args) throws Exception{ // 注意异常
        for(int i =0;i<5;i++) {
            System.out.println("当前时间"+new Date());
            Thread.sleep(1000);
        }       
    }
    }
  • yield():线程让步,也是Thread的静态方法,使得正在执行的线程暂停,但不会阻塞线程,只是交出CPU的控制权,将线程转为就绪状态,让系统调度器重新调度一次。当某个线程调用yield方法暂停后,只有优先级与当前线程相同,或者优先级比当前线程更高的线程才有可能获得执行机会。

  • 改变线程优先级:setPriority(int newPriority),高优先级的线程能获得更多的执行机会。

线程同步

  • 线程的同步的意义在于线程安全,也就是说有多个线程并发访问同一个对象,而线程调度的不确定性可能带来潜在的安全问题。

  • 同步监视器:java多线程引入同步监视器来解决同步问题,任何时刻只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。java允许任何对象作为同步监视器,通常我们使用可能被并发访问的共享资源作为同步监视器。

  • 同步代码块:显式指定同步监视器

    public class DrawThread extends Thread {
    private Account account;
    private double drawaccout;
    
    public DrawThread(String name,Account account,double drawaccount) {
        super(name);
        this.account = account;
        this.drawaccout= drawaccount;
    }
    
    public void run() {
        synchronized(account) {
            if(account.getBlance()>=drawaccount) {
                System.out.println(getName()+"取钱成功");
                try {
                    Thread.sleep(1);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    }
  • 同步方法:隐式指定同步监视器,使用synchronized关键字来修饰某个方法,此时该方法(非static方法)无需显式指定同步监视器,同步监视器默认为this,也就是调用该方法的对象

    public synchronized void draw(double amount) {
        ……
    }
  • 同步监视器的释放:线程会在以下几种情况下释放同步监视器的锁定。
  1. 当前线程的同步方法,同步代码块执行结束或者在执行中遇到break,return等终止了代码块的执行
  2. 同步代码块或者方法中出现未处理的Error或者Exception,导致异常结束
  3. **当前线程执行同步代码块或者同步方法时,程序中执行了同步监视器的wait()方法,wait是object的方法,范围是该object实例所在的线程
  • 同步锁:lock,更加强大的线程同步机制,通过显式定义锁对象来实现同步,也就是Lock对象,线程在访问共享资源之前,需要先获得锁对象。线程安全控制中比较常用的是ReetrantLock可重入锁。一个线程可以对已经加锁的ReetrantLock再度加锁。

    class X{
        private final ReentrantLock lock = new ReentrantLock();
    
        //需要定义线程安全的方法
        public void foo() {
            lock.lock();//加锁
            try {
                // 需要保证线程安全的代码
            }
            finally {
                lock.unlock();//使用finally块保证释放锁
            }
        }
    }
  • 死锁的问题:
    产生死锁的四个必要条件:
    (1) 互斥条件:一个资源每次只能被一个进程使用。
    (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种:
1) 预防死锁:这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。

2) 避免死锁:该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

3) 检测死锁:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。

4) 解除死锁:这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

java中应该避免死锁的出现。

线程安全是以牺牲程序运行效率为代价的,因此在注意线程安全的同时,也要注意不要滥用锁和同步方法,尽量只对那些会改变竞争资源的方法进行同步。同时还要根据单线程和多线程运行环境来提供线程不安全和线程安全两种版本,JDK提供的StringBuilder,StringBuffer就是一个例子。

线程通信

  • Object类提供的wait(),notify(),notifyAll()三个方法,由同步监视器来调用,对于同步方法,其同步监视器是默认实例this,可以再同步方法中直接调用这三个方法。
  1. wait(): 当前线程等待或者等待若干ms,当前线程自动释放同步监视器,线程进入等待状态(阻塞),直到其他线程调用了该同步监视器的notify()或者notifyAll方法。
  2. notify():唤醒在同步监视器上等待的单个线程,若有多个线程等待,则任意选择其中一个。
  3. notifyAll():唤醒在此同步监视器上等待的所有线程。
  • 使用Condition控制线程通信:使用Lock对象来保证同步问题时,我们可以使用Condition类来释放Lock以及唤醒其他等待线程。

    private final Lock lock = new ReentrantLock();
    // Condition实例绑定在一个Lock对象上
    private final Condition cond = lock.newCondition();
    
    public void Draw(double drawamount) {
        lock.lock();
        try {
            if(!flag)
                cond.await();//导致当前线程等待
            else {
                // ...
                cond.signalAll();// 唤醒其他线程
            }
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
  • 使用阻塞队列(BlockingQueue)控制线程通信:当生产者线程试图向Blocking Queue中放入元素时,如果队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。

线程池

  • 使用线程池执行线程任务的步骤是:
  1. 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
  2. 创建Runnable实现类或者Callable实现类的实例,作为线程的执行任务。
  3. 调用ExecutorService对象的submit方法来提交Runnable或者Callable实例。
  4. 当没有任务时,使用shutdown()方法来关闭线程池。

    public class Testjava{
    public static void main(String[] args)
    throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(6);
        Runnable target = ()->{
            for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()
                        + "的i值为:"+ i);
            }
        };      
        // 向线程池中提交两个线程
        pool.submit(target);
        pool.submit(target);        
        pool.shutdown();
    }
    }
  • Executors是一个工厂类,它包含了如下几个静态工厂方法来创建线程池。
    20150827155651746.png
  1. newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程
  2. newFixedThreadPool(int nThreads):创建一个可重用的,具有固定线程数的线程池
  3. newSingleThreadExecutor():创建一个单线程线程池
  4. newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它将在指定延迟后执行线程任务
  5. newSingleThreadScheduledExecutor():创建一个延迟执行的单线程线程池
  6. newWorkingStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,以充分支持多CPU并行能力。
  7. newWorkingStealingPool():根据CPU个数设置并行级别。

12367 返回ExecutorService对象,代表一个线程池,可以执行Runnable以及Callable对象所代表的线程,45返回的ScheduledExecutorService对象。

【Java8源码分析】线程池-Executor与ExecutorService的全面剖析

线程相关类

  • ThreadLocal:Thread Local Variable线程局部变量,为每个使用该变量的线程提供一个变量值的副本,从而隔离多线程程序的竞争资源。ThreadLocal类的用法很简单,它提供了三个public方法:
  1. T get():返回此线程局部变量当前线程副本的值
  2. void remove():删除此线程局部变量中当前线程的值
  3. void set(T value):设置副本值

    class Accout{
        private ThreadLocal<String> name = new ThreadLocal<>();
        public Accout(String str) {
            this.name.set(str);
        }
        public String getname() {
            return name.get();
        }
        public void setname(String str) {
            this.name.set(str);
        }
    }

    注意:ThreadLocal与其他同步机制都是为了解决访问同一资源冲突问题而出现的,但是侧重的领域不同,同步机制为实现多个线程对相同资源访问的并发安全性,ThreadLocal则是隔离多个线程之间的数据共享,从而避免竞争。

  • 线程安全的集合类:
  1. ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap都是线程不安全的,如果需要在多线程中对上述集合类进行存取,需要使用Collections提供的静态方法将其包装成线程安全的类。
  2. 更好的方法是使用 java.util.concurrent包下提供的大量支持高效并发访问的集合接口和实现类。如ConcurrentHashMap,ConcurrentLinkedQueue,ConcurrentLinkedDeque

转自:https://www.cnblogs.com/zoe-mine/p/7954605.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值