第二章 Java并行程序基础

本文摘自《实战Java高并发程序设计》,了解详细内容推荐购买原版书籍。

第二章 Java并行程序基础

2.1有关线程你必须知道的事

进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。是线程的容器,程序的实体。
线程:轻量级进程,是程序执行的最小单位。
使用多线程而不是多进程进行并发程序设计,因为线程之间切换和调度成本远小于进程。

NEW状态表示刚创建的线程,还未执行。Start()方法调用,才表示线程开始执行。执行时线程处于Runnable状态,表示线程所需资源已经备好。如果在执行的过程中遇到了synchronized同步块,则进入Blocked阻塞状态,线程暂停执行,直到获得请求的锁。Waiting和Timed_Waiting都表示等待状态,区别是Waiting会进入无时间限制的等待,TW有时限。一般,Waiting线程是在等待一些特殊事件。一旦等到,线程再次执行,进入Runnable状态。当线程执行完毕后,则进入Terminated状态,表示结束。

2.2初始线程:线程的基本操作

2.2.1新建线程
Thread t1 = new Thread();
t1.start();

start()方法新建线程并让这个线程执行run()方法。

Thread t1 = new Thread();
t1.run();

在当前线程中调用run()方法。
注意:不要用run()来开启新线程,他只会在当前线程中,串行执行run()中的代码。

Tread t1 =new Thead(){
	@Override
	public void run(){
		System.out.println("HEllo, i am t1");
	}
};
t1.start();

匿名内部类,重载run()方法。
考虑到Java是单继承的,继承本身也是一种很宝贵的资源,所以可以使用runnable接口实现同样操作。Runnable是一个单方法接口,只有一个run()方法。
单继承的意思是,一个子类不能有两个以上的父类。如果你想写一个类C,但这个类C已经继承了一个类A,此时,你又想让C实现多线程。用继承Thread类的方式不行了。(因为单继承的局限性)。此时,只能用Runnable接口。

public interface Runnable {
	public abstract void run();
}

此外,Thread类还有一个重要的构造方法:

public Thread(Runnable target)
public void run(){
	if (target!=null){
	target.run();
	}		
}

传入一个Runnable接口的实例,在start()方法调用时,新的线程就会执行Runnable.run()方法。
注意:默认的Thread.run()就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该干什么,更为合理。

public class CreateThread3 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new CreateThread3());
        t1.start();
    }
    @Override
    public void run() {
        System.out.println("oh,i am Runnable!");   
    }
}

上述代码实现runnable接口,并将该实例传入Thread。避免了重载Thread.run(),单纯使用接口定义Thread,是最常用的方法。

2.2.2终止线程

Thread有一个stop()方法,但是太过暴力,强行把执行到一半的线程终止,释放线程所持有的锁,可能会引起一些数据不一致的问题。
在这里插入图片描述
可以增加一个stopMe()方法,增加stopme标记。这样就没机会写坏对象了。

public static class ChangeObjectThread extends Thread {
    volatile boolean stopme = false;

    public void stopMe(){
        stopme = true;
    }
    @Override
    public void run(){
        while(true){
            if (stopme) {
                System.out.println("exit by stopme");
                break;
            }
            synchronized (u) {
                int v = (int) (System.currentTimeMillis() / 1000);
                u.setId(v);
                //Oh,do sth .else
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                u.setName(String.valueOf(v));
            }
            Thread.yield();
        }
    }
}
2.2.3线程中断

中断表面上就是让目标线程停止执行的意思,实际上并非如此。
线程中断不会使线程立即退出,而是给线程发送一个退出通知。至于目标线程收到后如何处理,则由目标线程自行决定。
与线程中断有关的三个方法:

public void Thread.interrupt()
public boolean Thread.isInterrupt()
public static boolean Thread.interrupted()

Thread.interrupt()方法是一个实例方法。通知目标线程中断,也就是设置中断标志位表示当前线程已经被中断了。Thread.isInterrupt()方法也是实例方法,判断当前线程有没有被中断(通过检查中断标志位)。静态方法Thread.interrupted()判断当前线程中断状态,同时清除中断标志位状态。
如果希望程序在中断后退出,就必须为它增加相应中断处理代码:

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run(){
                while (true) {
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("Interrupted!");
                        break;
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

Thread.sleep()函数签名如下:
Public static native void sleep(long millis) throws InterruptedExpection
“A native method is a Java method whose implementation is provided by non-java code.”
Thread.sleep()方法会让当前线程休眠,会抛出一个InterruptedExpection中断异常。InterruptedExpection不是运行时异常,也就是说程序必须捕获并处理它,当线程在sleep休眠时被中断,这个异常就会产生。


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run(){
                while (true) {
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("Interrupted!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted When Sleepp");
                        //设置中断状态
                        Thread.currentThread().interrupt();
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

注意:Thread.sleep()方法由于中断抛出异常,此时,它会清除中断标记,如果不加处理,在下一次循环开始时,就无法捕获这个中断,所以在异常处理中,再次设置中断标记位。

2.2.4等待(wait)和通知(notify)

× 为了支持多线程之间协作,JDK提供了接口线程等待wait()方法和通知notify()方法。这两个方法不在Thread类中,而在输出Object类。所以任何对象都可以调用。
  两个方法签名如下:
  Public final void wait() throws InterruptedExpection
  Public final native void notify()

  线程A调用了obj.wait()方法后,就会停止继续执行,转为等待状态。一直等到其他线程调用了obj.notify()方法为止。Obj对象就变成了多个线程中有效通信手段。
在这里插入图片描述
Object.notify()被调用时,会从等待队列中随机选择一个线程唤醒。
Obj.notifyAll()方法可以唤醒所有等待线程。
Object.wait()必须包含在对应的synchroniz的语句中,无论是wait()或notify()都需要获得目标Object的一个监视器(锁)。
在这里插入图片描述
注意:sleep()和wait()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另个区别就是wait()方法会释放目标对象的锁,而sleep()不会释放任何资源。

2.2.5 挂起(suspend)和继续执行(resume)线程

× 被标记为废弃方法,不推荐使用。原因是suspend()挂起线程时,不会释放任何锁资源。如果resume()操作意外的在suspend()之前就执行了,那么被挂起的线程可能很难有机会被继续执行。更严重的是:它占用的锁不会释放,可能导致整个系统工作不正常。
  被挂起的线程从状态上看,还是runnable,会影响对系统当前状态判断。
在这里插入图片描述
  Jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,生成java虚拟机当前时刻的线程快照。
  可以利用wait()和notify()方法在应用层面上实现suspend()和resume()功能。
  给出一个标记变量suspendme,表示当前线程是否被挂起。增加suspendMe()和resumeMe()两个方法,用于挂起和继续执行线程。
在这里插入图片描述

2.2.6等待线程结束(join)和谦让(yield)

很多时候,一个线程的输入可能非常依赖于另一个或多个线程的输出,这就需要等待依赖线程执行完毕。JDK提供join()操作实现。
两个join()方法:
Public final void join() throws InterruptedExpection
Public final synchronized void join(long millis) throws InterruptedExpection
第一个表示无限等待,一直阻塞当前线程,直到目标线程执行完毕。
第二个给出最大等待时间,超过时间目标线程还在执行,当前线程“等不及”继续执行。
在这里插入图片描述
  Join()的本质是让调用线程wait()在当前线程对象实例上。当线程执行完成后,被等待线程会在退出前调用notifyAll()通知所有等待线程继续执行。因此,值得注意的是:不要在应用程序中,在Thread对象实例上使用类似wait()或notify()等方法,这很可能影响系统API的工作,或被系统API影响。
  Thread.yield()方法:
  Public static native void yield();
  这是一个静态方法,一旦执行,它会使当前线程让出CPU,之后再进行CPU资源争夺。如果觉得一个线程不太重要,或者优先级非常低,又害怕它占用太多CPU资源,可以适当调用Thread.yield()方法。

2.3 volatile与Java内存模型(JMM)

* Java内存模型都是围绕原子性、有序性、可见性展开的。为了在特殊场合,确保线程之间的原子性、有序性、可见性。Java使用一些特殊操作或关键字来声明、告诉虚拟机,在这个地方要尤其注意,不能随便变动优化目标指令。关键字Volatile就是其中之一
  保证写入数据不坏,最简单就是加入volatile声明,告诉编译器(虚拟机)这个数据要格外小心,因为他会不断被修改。
在这里插入图片描述
  需要注意的是,volatile不能代替锁。也无法保证一些复合操作的原子性。
  此外,volatile也能保证数据的可见性和有序性。例子:
在这里插入图片描述
在这里插入图片描述
  在虚拟机client模式下,由于JIT没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出。这就是典型的可见性的问题。
  注意:可以使用Java虚拟机参数-server切换到server模式。
  可以用volatile来申明ready变量,告诉虚拟机这个变量可能在不同线程修改。

2.4 分门别类的管理:线程组

在一个系统中,如果线程数量比较多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组中。
  线程组的使用:
在这里插入图片描述
  其中activeCount()方法可以获得线程的总数,由于线程时动态的,这个值只是一个估计。.list()方法可以打印这个线程组的所有线程信息,对调试有帮助。
  线程组有stop()方法,停止线程组中所有线程。使用时谨慎。

2.5 驻守后台:守护线程(Daemon)

守护线程在后台默默完成一些系统性的服务,比如,垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的是用户线程,可以认为是系统的工作线程,他会完成这个程序应该要完成的业务操作。如果用户线程全部结束,就意味着这个程序实际无事可做了。守护线程也没有存在必要了,那么程序就应该自然结束。因此,在Java应用内,只有守护线程时,虚拟机会自然退出。
  守护线程使用:
在这里插入图片描述
  注意:守护线程必须在线程start()之前设置,否则失败报错,但继续执行,被当为用户线程。
在这里插入图片描述
  例子中t为守护线程,系统中只有主线程main为用户线程,因此,在main线程休眠2秒后退出时,整个程序也随之结束(只有守护线程)。如果不设置守护线程,t线程会一直打印。

2.6 先干重要的事:线程优先级priority

运气不好高优先级线程也可能抢占失败。Java中使用1-10(有效范围)表示线程优先级。高优先级的线程倾向于更快的完成。
  一般可以使用内置三个静态标量表示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.7 线程安全的概念与synchronized

volatile并不能真的保证线程安全。它只能确保一个线程修改数据后,其他线程能够看到这个改动。但当两个线程同时修改某一数据时,依然会产生冲突。
在这里插入图片描述
  期望最终结果为20000000,但最终值会小于20000000,。因为两个线程同时对i进行写入时,其中一个线程结果会覆盖另一个。(虽然i被声明为volatile变量)
在这里插入图片描述
  要解决这个问题,要保证多个线程对i操作时完全同步。==在A线程写入时,B不仅不能写,同时也不能读。
  关键字synchronized作用是实现线程间同步。它的工作是对同步的代码加锁,使得每一次只有一个线程能够进入同步块,从而保证线程的安全性。
  synchronized用法

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
    在这里插入图片描述
    上述代码也可以写成如下形式:(作用于实例方法)
    在这里插入图片描述
      在进入increase()方法前,线程必须获得当前对象实例的锁。本例中就是instance对象。注意14、15行Thread创建方式。使用Runnable接口创建两个线程,两个线程都指向同一个Runnable接口实例(instance对象),这样保证两个线程工作时,关注同一个对象锁,保证线程安全。
      一种错误的同步方式:
    在这里插入图片描述
    在这里插入图片描述
      两个线程指向了不同的Runnable实例。修改方式:将synchronized作用于静态方法。方法块要请求当前类的锁而非当前实例。

2.8 程序中的幽灵:隐蔽的错误

2.8.1 无提示的错误案例
2.8.2 并发下的ArrayList

ArrayList线程不安全容器。
在这里插入图片描述
上述程序执行可能三种结果:
一, 正常结束。
二, 程序抛出异常。
在这里插入图片描述
在这里插入图片描述
  ArrayList在扩容过程中,内部一致性被破坏,由于没有锁的保护,另一个线程访问到不一致的内部状态,导致出现越界。
  三、出现一个隐蔽的错误,比如打印ArrayList大小。

1793758

* 多线程访问冲突,保存容器大小变量被多线程不正常访问,同时两个线程同时对一个位置赋值。
  注意:改进方法:用线程安全的Vector替换ArrayList即可。

2.8.3 并发下诡异的HashMap

HashMap非线程安全。
在这里插入图片描述
在这里插入图片描述
上述代码期望得到map.size()=100000.实际上会有三种可能:
一、 程序正常结束,结果符合预期。
二、 程序正常结束,结果不对,小于100000.
三、 程序永远无法结束。
  前两种情况和ArrayList类似,第三种情况时,打开任务管理器,会发现这段代码占用了极高的CPU,最有可能占用了两个CPU核,并使得这两个CPU占用率100%。这非常类似于死循环的情况。
  使用jstack工具显示程序的线程信息。其中jps可以显示当前系统中所有的Java进程。而jstack可以打印给定Java进程的内部线程及其堆栈。
在这里插入图片描述
可以找到t1,t2和main线程:
在这里插入图片描述
在这里插入图片描述
  可以看到,主线程main处于等待状态,并且这个等待是由于join()方法引起的,符合预期。而t1和t2线程都处于Runnable状态,并且执行当前语句为HashMap.put()方法。查看put()方法的第498行代码,如下:
在这里插入图片描述
  可以看到,当前两个线程正在遍历HashMap内部数据。当前所处循环乍看是一个迭代遍历,就如同遍历一个链表一样。但是,由于多线程冲突,这个链表结构已经找到破坏,链表成环了!图2.9所示最简单一种环状结构。
在这里插入图片描述
  此死循环问题在JDK8中已经不存在了。Java8对HashMap内部实现做了大规模调整,规避了这个问题。但是,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。
  解决方法:使用ConcurrentHashMap。

2.8.4 初学者常见问题:错误的加锁

假设我么需要一个计数器,计数器会被多个线程同时访问:
在这里插入图片描述
在这里插入图片描述
  Integer数据不可变对象。也就是说对象一旦被创建就不能被修改。
  使用javap反编译这段代码的run()方法,可以看到:
在这里插入图片描述
  19-22行实际上使用了Integer.valueOf()方法新建了一个Integer对象,并赋值给i。i++实际为i=Integer.valueOf(i.intValue()+1);查看Integer.valueOf(),可以看到:
在这里插入图片描述
  Integer.valueOf()实际是工厂方法,倾向返回一个代表指定数值的Integer实例。在多线程之间,不一定能看到同一个对象(i对象一直在变)。加锁可能加在不同对象实例上。
  修正方法:将synchronized(i)给为synchronized(instance)即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值