java多线程(二)

一、控制线程

Java的线程支持提供了一些便捷的工具方法,通过这些便捷的工具方法可以很好的控制线程的执行。

1. join线程
Thread提供了一个让线程等待另一个线程完成的方法:join()方法。当某个程序执行过程中,调用另外一个线程的join()方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。
join()方法通常由使用线程的程序调用,将大问题划分成许多小问题,每个小问题分配一个线程,当所有的小问题都得到处理后,再调用主线程进一步操作。

join方法有三种重载形式:

  • join():等待被join的线程执行完成
  • join(long millis):等待被join的线程的时间最长为millis毫秒,若在millis毫秒内被join的线程还未执行结束,则不再等待
  • join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加上nanos微秒。这种方法很少用,因为程序对时间的精度无须精确到微秒,且操作系统本身也无法精确到微秒。

2. 后台线程
有一种线程,它在后台运行,它的任务是为其他线程提供服务,这种线程被称为后台线程,又称为守护线程或精灵线程
JVM的垃圾回收线程就是典型的后台线程。当所有的前台线程都死亡的时候,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。
Thread类还提供了isDaemon()方法来判断指定线程是否为后台线程。

注意:在设置某个线程为后台线程时,必须在该线程启动之前设置。

3. 线程睡眠:sleep
Thread类中的sleep方法可以将正在执行的线程暂停一段时间,并进入阻塞状态。sleep方法有两种重载的形式:

  • static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态
  • static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos微秒。同上,该方法很少用

4. 线程让步:yeild
yield()方法是和sleep方法相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。
实际上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同或者优先级比当前线程更高就绪状态的线程才会获得执行机会。

5. 改变线程优先级
每个线程执行时都有一定的优先级,每个线程默认的优先级都与创建它的父线程具有相同的优先级。默认情况下,main线程具有普通优先级,由main线程创建的子线程也有普通优先级。
Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围1到10,也可以使用Thread类中的三个静态常量:
MAX_PRIORITY:其值为10, MIN_PRIORITY:其值为1 , NORM_PRIORITY:其值为5。

二、线程同步

由于系统的线程调度具有一定的随机性,所以当多个线程来访问同一个数据时,非常容易出现线程安全问题。

1. 线程安全问题
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。

2. 同步代码块
出现线程安全问题的原因是run方法的方法体不具有同步安全性,程序中有多条并发线程在操作共享数据,就会出现问题。
为解决这个问题,Java多线程引入了同步监视器,使用同步监视器的方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj){
    ...
    //同步代码块
}

上面代码的含义是,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定
注意:任何时刻只能有一条线程获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放对该同步监视器的锁定。

同步监视器可以阻止两条线程对同一个共享资源进行并发访问,因此通常将可能被并发访问的共享资源当做同步监视器。synchronized代码块通常将run方法里的方法体修改成同步代码块,这样的做法符合“加锁,修改完成,释放锁”的逻辑,任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成,该线程释放对该资源的锁定。这样就可以保证并发线程在任一时刻只有一条线程可以进入修改共享资源的代码区。

3. 同步方法
同步方法就是使用synchronized关键字来修饰某个方法。同步方法的同步监视器是this。
通过同步方法可以将某类变成线程安全的类,线程安全的类有如下特征:

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该对象的任意方法之后都将得到正确结果
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

4. 释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,都必须先获得对同步监视器的锁定。但是程序无法显示释放对同步监视器的锁定,线程会在以下情况释放锁定:

  • 当前线程的同步方法、同步代码块执行结束
  • 当线程在同步代码块、同步方法中遇到了break、return终止代码块、方法的继续执行
  • 当线程在同步代码块、同步方法中出现了未处理的Error或Exception
  • 线程执行同步代码块或同步方法时,程序执行了同步监视器的wait()方法

下面情况下,线程不会释放同步监视器:

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器;
  • 其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。应当尽量避免使用suspend和resume方法来控制线程。

5. 同步锁(Lock)
JDK1.5之后,Java提供了另外一种线程同步的机制:通过显示定义同步锁对象来实现同步。通常认为Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象。
在线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。通常使用Lock对象的代码格式如下:

class X{
    // 定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        // 加锁
        lock.lock();
        try{
            // 需要保证线程安全的代码
        }
        // finally保证释放锁
        finally{
            lock.unlock();
        }
    }
}

当获取了多个锁的时候,它们必须按照相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。ReentrantLock锁具有重入性,就是线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()释放锁。

6. 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,所有线程处于阻塞状态,无法继续。
死锁的描述:假如有2个线程,一个线程想先锁对象1,再锁对象2,恰好另外有一个线程先锁对象2,再锁对象1。在这个过程中,当线程1把对象1锁好以后,就想去锁对象2,但是不巧,线程2已经把对象2锁上了,也正在尝试去锁对象1。
什么时候结束呢,只有线程1把2个对象都锁上并把方法执行完,并且线程2把2个对象也都锁上并且把方法执行完毕,那么就结束了,但是,谁都不肯放掉已经锁上的对象,所以就没有结果,这种情况就叫做线程死锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值