2 java并行基础
我们认真研究如何才能构建一个正确、健壮并且高效的并行系统。
进程与线程
进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程是线程的容器。程序是指令、数据和其组织形式的描述,进程是程序的实体。进程中可以容纳若干个线程。
进程和线程的关系:线程就是轻量级的进程,是程序执行的最小单位。为什么我们使用多线程而不是多进程?因为线程间的切换调度成本远远小于进程,所以我们使用多线程而不是多进程。
线程的生命周期
线程的所有状态都在Thread中的State枚举中定义。
public enum State{
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
NEW状态表示刚刚创建的线程,这种线程还没开始执行。start()方法调用时,线程开始执行。当线程执行时,处于RUNABLE状态,表示线程所需的一切资源都已经准备好了。
如果线程在执行过程中遇到了synchronized
同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。
waiting
和time_waiting
都表示等待状态,它们的区别是waiting
会进入一个无时间限制的等待,time_waiting
会进行有时间限制的等待状态。一般说,waiting的线程是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入runnable
状态。当线程执行完毕后,则进入terminated状态,表示结束。
注意:从new状态出发后,线程不能再回到NEW状态,同理,处以TERMINATED的线程也不能再回到RUNNABLE状态。
初始线程:线程的基本操作
这节了解一下java为线程操作提供的一些API。
新建线程
新建线程很简单,一种可以使用继承Thread,重载run()方法来自定义线程,下面是匿名内部类,也是重载了run()方法:
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("Hello, I am t1");
}
};
t1.start();
start()后,线程Thread,有一个run()方法,start()方法会新建一个线程并让这个线程执行run()方法。
t1.start()和t1.run()两个方法的区别:start()会开启新的线程,并调用run()执行线程;直接调用run()方法也能通过编译,却不能新建线程,而是在当前线程中调用run()方法(不要尝试这样开启新线程,它只会在当前线程中,串行执行run()中的代码)。
Thread t1 = new Thread();
t1.run();
第二种是使用Runnable接口来实现同样的操作。这种方法解决了java单继承实现多线程的缺点:
public class CreateThread implements Runnable {
@Override
public void run() {
System.out.println("Hi!I am Runnable");
}
public static void main(String args[]) {
Thread thread = new Thread(new CreateThread());
thread.start();
}
}
终止线程
一般,线程在执行完毕就会结束,无需手动关闭。特殊情况,需要手动关闭。
使用stop关闭
可以使用stop关闭,但是不推荐,为什么?原因是stop()太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。举个例子:
记录1:ID=1,name=小明
记录2:ID=2,name=小王
上面数据库中要么是存记录1,要么存记录2,否则说明数据被损坏了,在单线程中不会出现这种情况,单在多线程中则会出现这样的情况。
Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而此时,线程写到一半便终止了。由于锁被释放,等待该锁的读线程也可以读到这个不一致的数据。如下图:
代码模拟:
public class StopThreadUnsafe {
public static User user = new User();
public static class User {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User() {
id = 0;
name = "0";
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
public static class ChangeObjectThread extends Thread {
public void run() {
while (true) {
synchronized (user) {
int v = (int) (System.currentTimeMillis() / 1000);
user.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName(v + "");
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread {
public void run() {
while (true) {
synchronized (user) {
if (user.getId() != Integer.parseInt(user.getName())) {
System.out.println(user.toString());
}
}
Thread.yield();
}
}
}
public static void main(String args[]) throws InterruptedException {
new ReadObjectThread().start();
while (true) {
Thread thread = new ChangeObjectThread();
thread.start();
Thread.sleep(150);
thread.stop();
}
}
}
上面程序本来应该输出id和name的值都相同,但是却输出如下的错误数据,这种错误没有报错,很难查找。
User{name='1565947644', id=1565947645}
User{name='1565947644', id=1565947645}
如何解决上面的问题?需要由我们自行决定线程何时退出。仍然用本例说明,只需要将ChangeObjectThread线程增加一个stopMe()即可:
public static class ChangeObjectThread extends Thread {
volatile boolean stopme = false;
public void stopMe(){
stopme = true;
}
public void run() {
while (true) {
//手动停止线程
if (stopme){
System.out.println("exit by stop me");
break;
}
synchronized (user) {
int v = (int) (System.currentTimeMillis() / 1000);
user.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName(v + "");
}
Thread.yield();
}
}
}
线程中断
线程中断可以和stop()一样起到退出线程的作用,但是它不会立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于是否退出由目标线程自行决定。
与线程中断的三个方法:
public void interrupt() //中断线程
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
如果不手动加入中断处理的逻辑,即使对线程中断,这个中断也不会起任何作用。
public class T1 {
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("Interruted!");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interrupted when Sleep");
Thread.currentThread().interrupt(); //1
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
如果去掉上例代码run方法中的Thread.sleep(2000),那么看起来和之前的stopme的方案很相似,但是中断的功能更强大。如果在循环体中,类似于wait()或者sleep()这样的操作,只能通过中断来识别。
若在线程休眠期间发生中断,它会抛出一个InterruptedException中断异常,并且清除中断标记。在上例代码中,1处是在捕获异常后(此时已清除了中断标记)重新设置中断标志,使其在下一次循环进入if语句中断循环。
等待(wait)和通知(notify)
public final void wait() throws InterruptedException
public final native void notify()
当一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。直到等到其他线程调用了obj.notify为止。显然,这个对象成为了多个线程之间的有效通信手段。
wait()和notiry()如何工作?
如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程将其唤醒。这个选择完全是随机的。而object.notifyAll()会将这个等待队列中所有等待的线程唤醒,而不会随机一个。
Object.wait()方法不是随便调用的,必须包含在对应synchronzied语句中,无论是wait()或者notify()都需要首先获得目标对象的一个监视器。下图展示了wait()和notify()的工作流程。
下面代码简单的使用了wait()和notify(),T1执行了object.wait()方法,这时立马释放对象锁。此时正在等待对象锁的T2捕获到后,在2处执行object.notify()方法,但是此时和object.wait()方法不同,不立马释放,而是执行完synchronized块的代码后才释放。T2释放后,T1再次捕获,执行T2接下来的程序。
public class SimpleWN {
final static Object object = new Object();
public static class T1 extends Thread{
public void run()
{
synchronized (object) {
System.out.println(System.currentTimeMillis()+":T1 start! ");
try {
System.out.println(System.currentTimeMillis()+":T1 wait for object ");
object.wait(); //1 wait()后,马上释放对象锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+":T1 end!");
}
}
}
public static class T2 extends Thread{
public void run()
{
synchronized (object) {
System.out.println(System.currentTimeMillis()+":T2 start! notify one thread");
object.notify(); //2 notify()后,没有马上释放对象锁,而是执行完synchronized块的代码后释放
System.out.println(System.currentTimeMillis()+":T2 end!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println(System.currentTimeMillis()+":T2 after sleep!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(System.currentTimeMillis()+":T2 after synchronized!");
}
}
public static void main(String[] args) {
Thread t1 = new T1() ;
Thread t2 = new T2() ;
// Thread t1_1 = new T1() ;
// t1_1.start();
t1.start();
t2.start();
}
} /**
1566280793124:T1 start!
1566280793125:T1 wait for object
1566280793127:T2 start! notify one thread
1566280793127:T2 end!
1566280796127:T2 after sleep!
1566280796128:T1 end!
1566280797127:T2 after synchronized!
*/
Object.wait()
与Thread.sleep()
的区别:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另外一个主要区别就是wait()方法会释放对象的锁,而Thread.sleep不会。
挂起(suspend)和继续执行(resume)线程
被挂起的线程,必须要等待resume()后,才能继续执行。
这对方法已经不推荐使用了。不推荐的原因是因为suspend()在导致线程暂停的同时,并不会释放任何锁资源,直到等到resume()才释放。如果resume()操作意外出现在suspend()前面,就可能导致永久挂起。。此时,任何其他线程想要访问被它占用的锁时,都会受到牵连。甚至整个系统运行不正常。
public class BadSuspend {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println("in "+getName());
Thread.currentThread().suspend(); //1
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.resume();
System.out.println("t1 resume!");
t2.resume();
System.out.println("t2 resume!");
t1.join();
t2.join();
}
}/**
in t1
t1 resume!
t2 resume!
in t2
*/
上面程序中,t1.start()后线程走到1处被挂起,此时不释放对象锁。t2.start()后,t2需要等待t1释放的对象锁。在t2等待锁的过程中,t2.resume()已经发生了(通过打印结果可以看出),随后才发生t2的Thread.suspend()。这时,t2被永久挂起。
那如果需要一个比较可靠的suspend()函数,该怎么做呢?可以利用wait()和notify(),在应用层面实现suspend()和resume():
public class GoodSuspend {
public static Object u = new Object();
public static class ChangeObjectThread extends Thread {
// 标记变量,表示当前线程是否被挂起
volatile boolean suspendme = false; //1
// 挂起线程
public void suspendMe(){
suspendme = true;
}
// 继续执行线程
public void resumeMe(){
suspendme = false;
synchronized (this){
notify();
}
}
@Override
public void run(){
while (true){
synchronized (this){ //2
while (suspendme){
try {
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
synchronized (u){
System.out.println("in ChangeObjectThread");
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread{
@Override
public void run(){
while (true){
synchronized (u){
System.out.println("in ReadObjectThread");
}
Thread.yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
ChangeObjectThread t1 = new ChangeObjectThread();
ReadObjectThread t2 = new ReadObjectThread();
t1.start();
t2.start();
Thread.sleep(1000);
t1.suspendMe();
System.out.println("suspend t1 2 sec");
Thread.sleep(2000);
System.out.println("resume t1");
t1.resumeMe();
}
}
在1处,给出了一个标记suspendme,表示当前线程是否被挂起,同时,增加了suspendMe()(通过执行wait()方法实现挂起)和resumeMe()(通过执行notify()通知继续执行,并清除挂起标记),注意,2处给自己加锁。
等待线程结束(join)和谦让(yield)
join
在多线程中,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行,jdk提供了join()
来实现这个功能。有2个join方法
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一个join()表示无限等待,它会阻塞当前线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程就会不管继续往下执行。
join()的本质是让调用线程wait()在当前线程对象实例上。下面是JDK中join()实现的核心代码片段:
while(isAlive()){
wait(0);
}
它让调用线程在当前线程对象上等待。当线程执行完成后,会在推出前调用notifyAll()通知所有等待线程继续执行。
Thread.yield()
public static native void yield();
这是一个静态方法,它会让当前线程让出CPU。在让出CPU后,还会进行CPU资源的争夺,至于是否能再次分配,就不一定了。它的调用好像是在说:我已经完成了一些重要的工作了,我可以休息一下,给其他线程一些工作机会。
volatile与java内存模型(JMM)
之前提到过:java内存模型围绕原子性、有序性和可见性展开。
为了在适当的场合,确保线程间的有序性、可见性和原子性。java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,这个地方要特别注意,不能随意变动优化目标指令。关键字volatile
就是其中之一。
使用volatile去申明一个变量,可以保证这个变量的可见性的特点。在之前的例子MultiThreadLong中,long型的 t 改为volatile,使其保证了原子性。
public class MultiThreadLong {
public volatile static long t=0;
public static class ChangeT implements Runnable{
......
volatile对于保证操作的原子性有非常大的帮助,但是volatile并不能替代锁,它无法保证一些复合操作的原子性。如下例中,无法保证i++的原子操作
public class PlusTask implements Runnable {
public volatile static Integer j = 0; //1
public void add(){
for (int i = 0; i < 10000; i++) {
j++;
}
}
@Override
public void run() {
// synchronized (PlusTask.class) {
add();
// }
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
PlusTask task = new PlusTask();
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new PlusTask());
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join();
}
System.out.println(j);
}
}
上面的代码中,最终的值应该是100000,但实际总是会小于期望值。
volatile除了可以保证原子性,也能保证数据的可见性和有序性。下面看一个例子:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready);
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(10000);
}
}
由于系统优化的结果,ReaderThread线程可能无法“看到”主线程的修改,导致ReaderThread永远无法退出,这是一个典型的可见性问题。但是,只要简单使用volatile来申明ready变量,告诉java虚拟机,这个变量可能会在不同的线程中修改。就可以解决问题了。
分门别类的管理:线程组
在一个系统中,如果线程数量很多,而且功能分配明确,就可以将相同功能的线程放置在一个线程组中,方便管理。
线程组的使用很简单:
public class ThreadGroupName implements Runnable {
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("PrintGroup"); //1 建立名为“PrintGroup”的线程组
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start();
t2.start();
System.out.println(tg.activeCount()); //2
tg.list(); //3
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-"
+ Thread.currentThread().getName();
while (true) {
System.out.println("I am " + groupAndName);
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
代码1处,建立一个“PrintGroup”的线程组,并将T1和T2两个线程加入这个组中。2、3处,activeCount()可以获得活动线程的数量,list()打印线程组中所有线程信息。
线程组有一个stop(),它会停止线程组中所有的线程,但是和Thread.stop()会有相同的问题,要格外谨慎使用。
驻守后台:守护线程(Daemon)
守护线程是一种特殊的线程,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相应的是用户线程,它完成业务操作。如果用户线程全部结束,守护线程的对象不存在了,那么整个应用程序就应该自然结束。因此,在java引用内,只有守护线程时,java虚拟机会自然退出。
下面是一个简单的守护线程:
public class DaemonDemo {
public static class DaemonT extends Thread{
public void run(){
while(true){
System.out.println("I am alive");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t=new DaemonT();
t.setDaemon(true); //1
t.start();
Thread.sleep(2000); //当主线程执行完毕后,守护线程t也随之结束。
}
}
1处将t设置成守护线程,当主线程执行完毕后,守护线程也随之结束。若不把t设置成守护线程,那么程序永远不会结束。
先干重要的事:线程优先级
java中线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这是一个概率问题。这种优先级产生的后果不容易预测,优先级低的线程可能会导致饥饿现象(即使是优先级低,但是也不能饿死它啊)。因此,在要求严格的场合,还是需要自己在应用层解决线程调度问题。
在java中,使用1到10表示线程优先级。一般可以使用内置的三个静态变量标量表示:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
数字越大则优先级越高,但有效范围在1到10之间。例子:
public class PriorityDemo {
public static class HightPriority extends Thread{
static int count=0;
public void run(){
while(true){
synchronized(PriorityDemo.class){
count++;
if(count>10000000){
System.out.println("HightPriority is complete");
break;
}
}
}
}
}
public static class LowPriority extends Thread{
static int count=0;
public void run(){
while(true){
synchronized(PriorityDemo.class){
count++;
if(count>10000000){
System.out.println("LowPriority is complete");
break;
}
}
}
}
}
/**
* HightPriority先完成的次数多,但是 不保证
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
Thread high=new HightPriority();
LowPriority low=new LowPriority();
high.setPriority(Thread.MAX_PRIORITY); //1
low.setPriority(Thread.MIN_PRIORITY); //2
low.start();
high.start();
}
}
上述代码中1、2处设置了线程的优先级,所以总是高优先级的线程执行得会快些。
线程安全的概念与synchronized
并发程序开发的一大关注重点就是线程安全。程序并行化是为了获得更高的执行效率,同时保证程序的正确性。因此,线程安全是并行程序的根本和根基。
public class AccountingVol implements Runnable {
static AccountingVol accountingVol = new AccountingVol();
static volatile int i = 0;
public static void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(accountingVol);
Thread t2 = new Thread(accountingVol);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上述代码中,线程t1、t2可能同时读取i为0,并各自计算得到i=1,并先后写入这个结果,因此,虽然i++被执行了2次,但实际i的值只增加了1。
要解决这个问题,我们就要保证多个线程对i进行操作时完全同步。就是说,当线程A在写入时,B不仅不能写,也不能读。java中,提供了一个重要的关键字synchronized
来实现这个功能。
synchronized
作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每次只能有一个线程进入同步块。
用法:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
指定加锁对象
下面程序中,将synchronized作用于给定对象instance。每次进入被synchronized包裹的代码段,都会请求instance的锁。若有其他线程占用,则必须等待。
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
for(int j=0;j<10000000;j++){
synchronized(instance){
i++;
}
}
}
//main程序见上例代码
}
直接作用于实例方法
public class AccountingSync2 implements Runnable{
static AccountingSync2 instance=new AccountingSync2();
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<10000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync2 i1=new AccountingSync2();
// AccountingSync2 i2=new AccountingSync2();
Thread t1=new Thread(i1);
Thread t2=new Thread(i1);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
上例代码中,synchronized关键字作用于一个实例方法,这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在此例中,线程t1和t2需要用到相同的Ruanable实例i1,这样才能关注到同一个对象锁上。若两个线程使用不同的两个Runnable实例t1,t2,即两个线程使用了两把不同的锁。
但是,我们可以把increase()方法改成static的,这样方法块请求的是当前类的锁,而不是当前实例的,因此,线程可以同步。如下:
public class AccountingSync2 implements Runnable{
static AccountingSync2 instance=new AccountingSync2();
static int i=0;
public static synchronized void increase(){ //3
i++;
}
@Override
public void run() {
for(int j=0;j<10000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync2 i1=new AccountingSync2();
AccountingSync2 i2=new AccountingSync2();
Thread t1=new Thread(i1); //1
Thread t2=new Thread(i2); //2
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
1和2处使用了两个不同的Runable实例,但是3处的同步方法为static的,此方法需要的是当前类的锁而非当前实例的锁,因此线程间可以正确同步。
除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。被synchronized限制的多个线程死串行执行的。
程序中的幽灵:隐蔽的错误
有异常的异常堆栈好修复,但是,没有异常、没有日志、没有堆栈的异常,就很让人抓狂了。
无提示的错误案例
如果你运行下面的程序,会发现一个隐藏的错误:
int v1 = 1073741827;
int v2 = 1431655768;
int ave = (v1+v2)/2;
把上面ave打印出来,会发现ave的值是-894784850,一个负数。那是因为溢出。这种隐形的错误很难找,和幽灵一般。
并发下的ArrayList
ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错,那会出现哪些问题呢?
public class ArrayListMultiThread {
static ArrayList<Integer> al = new ArrayList<Integer>(10);
public static class AddThread implements Runnable{
@Override
public void run() {
for (int i=0;i<10000000;i++){
al.add(i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddThread());
Thread t2 = new Thread(new AddThread());
t1.start();t2.start();
t1.join(); t2.join();
System.out.println(al.size()); //抛出异常 返回小于2000的数值。
}
}
在上面这段错误的代码中,原本输出的应该是20000000,但是由于ArrayList不支持,我们可能会得到3中不同的结果:
第一,程序正常结束,几率极小
第二,程序抛出异常:
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 1823230
at java.util.ArrayList.add(ArrayList.java:459)
at geym.ch2.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:11)
at java.lang.Thread.run(Thread.java:745)
这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另一个线程访问到了不一致的内部状态,导致了越界问题。
第三,出现一个非常隐蔽的问题,打印的值小于期望值20000000。
!!改进的方法很简单,使用线程安全的Vector代替ArrayList即可。
并发下诡异的HashMap
HashMap同样不是线程安全的。
public class HashMapMultiThread {
static Map<String, String> map = new HashMap<String, String>();
public static class AddThread implements Runnable {
int start = 0;
public AddThread(int start) {
this.start = start;
}
@Override
public void run() {
for (int i = 0; i < 100000; i += 2) {
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
public static void main(String[] args) throws InterruptedException {
// 根据你的电脑CPU核数来配置 两核启两个线程就行
Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.size());
}
}
}
我们期望得到100000,但是,实际可能会有三种情况:
- 第一,程序正常结束,大小为预期值。
- 第二,程序正常结束,但是小于100000。
- 第三,程序永远无法结束
前面两种情况和ArrayList类似,对于第三种情况,由于多线程的冲突,HashMap中的Entry<K,V>
链表的结构遭到破坏,链表成环了!当链表成环时,HashMap.put()方法中的迭代就等于死循环。如图,展示了最简单的环状结构,key1和key2互为对方的next元素。
初学者常见问题:错误的加锁
public class BadLockOnInteger implements Runnable {
public static Integer i = 0;
public static BadLockOnInteger instance = new BadLockOnInteger();
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized (i){ //1
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
上面代码1处,把锁加在了i上,似乎并没有什么问题,然而,我们运行程序,却得到了比预期值20000000要小的数,这是为什么呢?因为Integer属于不可变对象。就是说Integer的值不能被修改,如果要修改,就要新建一个Integer对象。这样在多个线程间,并不一定能够看到同一个i对象(i一直在变),每次加锁都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。
修正这个问题,只要把锁加在instance上就可以了:
synchronized (i){
改为下面代码即可:
synchronized (instance){
2 java并行基础的更多相关文章
- Java并发程序设计(二)Java并行程序基础
Java并行程序基础 一.线程的生命周期 其中blocked和waiting的区别: 作者:赵老师链接:https://www.zhihu.com/question/27654579/answer/1 ...
- JAVA并行程序基础
JAVA并行程序基础 一.有关线程你必须知道的事 进程与线程 在等待面向线程设计的计算机结构中,进程是线程的容器.我们都知道,程序是对于指令.数据及其组织形式的描述,而进程是程序的实体. 线程是轻量级 ...
- JAVA并行程序基础二
JAVA并行程序基础二 线程组 当一个系统中,如果线程较多并且功能分配比较明确,可以将相同功能的线程放入同一个线程组里. activeCount()可获得活动线程的总数,由于线程是动态的只能获取一个估 ...
- JAVA并行程序基础一
JAVA并行程序基础一 线程的状态 初始线程:线程的基本操作 1. 新建线程 新建线程只需要使用new关键字创建一个线程对象,并且用start() ,线程start()之后会执行run()方法 不要直 ...
- Java 并发基础
Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...
- Java多线程--基础概念
Java多线程--基础概念 必须知道的几个概念 同步和异步 同步方法一旦开始,调用者必须等到方法调用返回后,才能执行后续行为:而异步方法调用,一旦开始,方法调用就立即返回,调用者不用等待就可以继续执行 ...
- 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!
本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...
- Java 多线程基础(一)基本概念
Java 多线程基础(一)基本概念 一.并发与并行 1.并发:指两个或多个事件在同一个时间段内发生. 2.并行:指两个或多个事件在同一时刻发生(同时发生). 在操作系统中,安装了多个程序,并发指的是在 ...
- 思维导图学《On Java》基础卷 + 进阶卷
说明 目录 思维导图 导读 第 1 章 什么是对象 第 3 章 一切都是对象 第 6 章 初始化和清理 第 7 章 实现隐藏 第 8 章 复用 第 9 章 多态 第 10 章 接口 第 11 章 内部 ...
随机推荐
- linux 的 expect 自动交互
https://www.jianshu.com/p/0194cbd70d39 https://www.cnblogs.com/saneri/p/10819348.html 参考 expect是一个自 ...
- 3-10 Pandas 常用操作
1.构造数据 In [1]: import pandas as pd data=pd.DataFrame({'group':['a','a','a','b','b','b','c','c','c' ...
- java1.8新特性整理(全)
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/yitian_66/article/deta ...
- day37_8_21表的查询
一.语法 表的查询一般使用select关键字,配合where筛选.顺序如下: # 先后顺序 from where select 二.where约束条件 首先先建立表: create table emp ...
- day15_7.17正则表达式与re模块
一.正则表达式 在用户登录注册,以及身份验证时,会发现,如果在手机号的窗口输入字母等不是手机号的格式的字符串时,会报错,这种筛选字符串的功能就是由正则表达式提供. 正则表达式是几乎所有编程语言都会设计 ...
- 用Jetpack的Site Accelerator为网站CDN加速
Jetpack 的Site Accelerator站点加速器(前身为 Photon,注意:“Photon”现在是站点加速器的一部分)允许 Jetpack 优化图像并通过他们的全球服务器网络CDN提供图 ...
- httpHandlers path="*.sky"
<httpHandlers> <add verb="*" path="*.sky" type="WebAppHttpHandlerT ...
- zz斯坦福Jure Leskovec图表示学习:无监督和有监督方法
斯坦福Jure Leskovec图表示学习:无监督和有监督方法(附PPT下载) 2017 年 12 月 18 日 专知 专知内容组(编) 不要讲得太清楚 [导读]现实生活中的很多关系都是通过图的形式 ...
- Node.js 入门篇
Node.js 使用C++开发的. Node.js是一个事件驱动服务端JavaScript环境,只要能够安装相应的模块包,就可以开发出需要的服务端程序,如HTTP服务端程序.Socket程序等. No ...
- [NOI2019]回家路线(最短路,斜率优化)
终于把这鬼玩意弄完了-- 为什么写的这么丑-- (顺便吐槽 routesea) 最短路的状态很显然:\(f[i]\) 表示从第 \(i\) 条线下来的最小代价. 首先明显要把那个式子拆开.直觉告诉我们 ...