多线程进阶(一)

这里写图片描述
当线程被创建并且启动后,它经历了5种状态:新建、就绪、运行、阻塞和死亡状态。当线程在运行的时候,不能一直占有CPU时间片,CPU会在多个线程之间进行调度,线程的状态也会多次切换于阻塞和运行状态。
当线程对象被创建出来是进入了新建状态,当调用了start方法后,线程进入就绪状态。这里可能读者的理解是线程start后进入运行状态,其实线程内部还是依赖JVM的调度,当调用了start方法后,JVM会认为这个线程可以执行,至于什么时候执行取决于JVM的内部调度。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个被线程被唤醒 (notify)后,才会进入到就绪队列,等待获得锁。
当一开始线程a第一次执行account.add方法时,jvm会检查锁对象account 的就绪队列是否已经有线程在等待,如果有则表明account的锁已经被占用了,由于是第一次运行,account的就绪队列为空,所以线程a获得了锁, 执行account.add方法。如果恰好在这个时候,线程b要执行account.withdraw方法,因为线程a已经获得了锁还没有释放,所以线程 b要进入account的就绪队列,等到得到锁后才可以执行。
一个线程执行临界区代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁
可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

notify 不会释放锁,而是通知锁对象的阻塞队列里的某一线程(被阻塞,即主动调用wait方法),进入就绪队列。

线程释放锁的方式,通常是 主动调用wait方法、同步代码块结束释放锁资源。

notifyall 是 唤醒阻塞队列里的所有阻塞线程,他们都将进入就绪队列,而notify的数量是一个。

同步代码块结束释放锁资源,对象就绪队列中的某一线程获得锁资源而开始线程;
如果不使用notify 那么阻塞队列 里 线程将一直处于阻塞状态,即使就绪队列里 线程都执行完了,阻塞队列 里 线程也将一直处于阻塞状态。

如果就绪状态的线程获取了CPU,那么这个线程处于运行状态,当这个线程运行时,不会一直霸占CPU,线程在执行的过程中会被在CPU上调度下来,以便其他线程能够获取执行机会。
线程进入阻塞状态的情况:
线程调用一个阻塞方法,方法返回前该线程一直阻塞。
线程调用sleep方法进入阻塞。
线程尝试获取同步监视器,但该同步监视器被其他线程持有。
线程调用了suspend方法挂起。
线程解除阻塞,重新进入就绪状态的情况:
调用的阻塞方法返回。
调用的sleep到期。
线程成功获取同步监视器。
被suspend的方法挂起的线程被调用了resume方法恢复。
线程的死亡状态就是线程的结束,线程结束的情况有如下几种:
run方法执行完成
线程抛出异常
直接调用线程的stop方法结束线程
判断线程是否死亡可以使用isAlive方法,当线程处于就绪、运行和阻塞三种状态的时候返回true,否则返回false。另外,不能对已经死亡的线程重新调用start方法重新启动。
另外,线程的suspend方法和stop方法非常容易导致死锁,一般不推荐使用。

线程的挂起和恢复:
线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行。
run() 和start() 是大家都很熟悉的两个方法。把希望并行处理的代码都放在run() 中;stat() 用于自动调用run(),这是JAVA的内在机制规定的。当一个线程进入“非可执行”状态,必然存在某种原因使其不能继续运行,这些原因可能是如下几种情况:
A,通过调用sleep()方法使线程进入休眠状态,线程在指定时间内不会运行。
B,通过调用join()方法使线程挂起,如果某个线程在另一个线程t上调用t.join(),这个线程将被挂起,直到线程t执行完毕为止。
C,通过调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态。

join线程
当一个线程需要等待另一个线程完毕再执行的话,可以使用Thread的join方法。
假设线程A和线程B,在A执行时调用了B的join方法,A将被阻塞,一直等到B线程执行完毕后,A线程继续执行,就好像排队加塞。

synchronized关键字可以修饰方法、代码块,但是不能修饰构造方法和属性。

线程要进入同步代码块或同步方法中,必须先获得同步监视器的锁定。也就是说必须先拿到锁,然后进入方法。那么什么时候释放锁呢?
1.方法执行结束
2.在方法中遇到Exception,导致异常
3.程序中遇到了退出程序的代码,比如return
4.程序执行了同步监视器对象的wait()方法

java的动态并发库,设计到三个包:
java.util. concurrent
这个包提供了并发编程中的一些实用工具类
java.util.concurrent.atomic
这个包支持在单个变量上解除锁的线程安全编程
java.util.concurrent.locks
这个包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。

java.util.concurrent.atomic
AtomicInteger的常用方法如下:

addAndGet(int delta) :以原子方式将给定值与当前值相加。 ++n
compareAndSet(int expect, int update):如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
decrementAndGet() : 以原子方式将当前值减 1。 –i
getAndAdd(int delta) :以原子方式将给定值与当前值相加。 n++
getAndDecrement() : 以原子方式将当前值减 1。 i–
getAndIncrement() : 以原子方式将当前值加 1。 I++
getAndSet(int newValue): 以原子方式设置为给定值,并返回旧值。
incrementAndGet() : 以原子方式将当前值加 1。

然而,这里面的这些方法,都是依赖于compareAndSet(int expect, int update)这个方法来实现的,而compareAndSet既是CAS的简称。前面也提到过了:

CAS指令需要三个操作数,分别是内存位置V,旧的预期值A,新值N。
使用CAS时,当V符合A时,使用N更新V的值,否则就不执行更新操作。无论是否更新,都返回V的值,这整个是一个原子操作。
而在JDK1.5之后,java程序可以使用CAS操作。不过这个类是在sun.misc.Unsafe里面的compareAndSwapInt等这些方法中实现的。
我们来看一下我们用过的getAndIncrement这个方法的源代码的实现:

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
}
    public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

incrementAndGet方法在一个死循环中—for(;;),这里得到当前值current后,然后使用临时变量next,让当前值加一。然后调用compareAndSet方法。而compareAndSet方法调用了unsafe的compareAndSwapInt。这里其实就是不断尝试得到一个比当前值大1的新值,然后将这个值赋给自己,如果失败的话,进入下一次循环一直到设置成功为止。

java.util.concurrent.locks
这个包涉及到的内容主要包含了几个锁的概念。synchronized,这种方式也能够实现类似锁的功能,只是在代码上不够面向对象。这里是更面向对象的一种锁的方式,除了这些,还有条件锁与读写锁。
再来看这个包中提供的接口,主要有三个:
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Condition 提供了条件锁
ReadWriteLock 提供了读写锁

Lock接口有一个实现类ReentrantLock。一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义

/**
 * 测试多线程情况下使用同步代码块
 * @author 
 */
public class SafeThreadTest {

    V v = new V();
    public static void main(String[] args) {
        SafeThreadTest test = new SafeThreadTest();
        test.test();
    }
    /**
     * 开两个线程,分别调用V对象的打印字符串的方法
     */
    public void test(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    v.printString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    v.printString("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
                }
            }
        }).start();
    }

/**
     * 这个类负责打印字符串
     * @author Administrator
     */
    class V {
        //创建一个锁对象
        Lock lock = new ReentrantLock();
        /**
         * 为了能使方法运行速度减慢,我们一个字符一个字符的打印
         * @param s
         */
        public void printString(String s){
            //加锁,只允许一个线程访问
            lock.lock();
                try {
                    for(int i = 0;i<s.length();i++){
                        System.out.print(s.charAt(i));
                    }
                    System.out.println();
                }finally{
                    //解锁,值得注意的是,这里锁的释放放到了finally代码块中,保证解锁工作一定会执行
                    lock.unlock();
                }
        }
    }
}

使用这样的方式,与使用synchronized的功能一样,只不过这样使代码看起来更加面向对象一些,怎么加锁,怎么解锁一目了然。
另外,ReentrantLock其实比synchronized增加了一些功能,主要有:
等待可中断
这是指的当前持有锁的线程如果长期不释放锁,正在等待的线程可以放弃等待,处理其他事情。
公平锁
这个是说多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序依次获得锁。synchronized中的锁是不公平锁,锁被释放的时候任何一个等待锁的线程都有机会获得锁,ReentrantLock默认也是不公平锁,可以使用构造函数使得其为公平锁。如果为true代表公平锁Lock lock = new ReentrantLock(true);
绑定条件
这个就是后面章节中会详细讲解的条件锁。
在性能上,在jdk1.6之前的版本,使用ReentrantLock的性能要好于使用synchronized。而在jdk1.6开始,两者性能均差不多。
这里写图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值