并发编程高级篇

目录

  • 线程安全
  • synchronized&volatile
  • 同步类容器、并发类容器"Concurrent"、“CopyOnWrite”
  • Queue
  • 生产者消费者模式
  • Executors线程池
  • JMS规范
  • ActiveMQAPI讲解
  • ActiveMQ高级主题(点对点模式/发布与订阅模式)
  • 多线程+ActiveMQ负载均衡实战

并发编程学习目的

我们为什么要去学习并发编程?

第一点:面试非常重要,企业面试程序员标准,考虑因素:

  1. 考虑我公司技术你是否熟悉50%以上,或者我们公司有特殊的技术需求,正好你熟悉,那么可能会考虑录用你。
  2. 细节、态度、人品问题。(1、2条件满足基本上就会录用你)
  3. 知识面,潜力(这是加分项)

第二点:对自己的技术提升很有帮助。也是我们最受益的一点,就是并发编程技术你学到手了,有了一个知识面的扩展,眼界更宽了。

第三点:如果你学习好了并发编程,在以后的分布式系统中,你都可以找到类似并发、分布式、并行处理问题的概论。

我们该如何学习并发编程呢?

在公司其实很多java程序员,亦或者是所谓的技术Leader,他们可能知道多线程中有 synchronizedvolatileReentrantLockconcurrent 下数据包等等…这些看似高深的代名词,但是不等于他们就会懂得如何去使用,滥用的结果往往需要自己承担的响应的后果。其实并发编程没有我们想象的那么复杂,我们只需要掌握最基本的概念就可以很轻松的入门,然后从中剖析这些概念的本质,结合实际业务逻辑去应用上去,那么你就会成为并发编程方面的专家。

线程安全

线程安全概念:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

示例
package cn.itcast_01;

public class MyThread extends Thread {
    
    

	private int count = 5;

	// synchronized加锁
	public synchronized void run() {
    
    
		count--;
		System.out.println(this.currentThread().getName() + " count = " + count);
	}

	public static void main(String[] args) {
    
    
		/*
		 * 当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),
		 * 一个线程想要执行synchronized修饰的方法里的代码:
		 * 		1、首先是尝试获得锁
		 * 		2、如果拿到锁,执行synchronized代码体内容:拿不到锁,这个线程就会不断的尝试获得这个锁,直到拿到为止。
		 * 		     而且是多个线程同时取竞争这把锁。(也就是会有索竞争的问题)。
		 */

		MyThread myThread = new MyThread();
		Thread t1 = new Thread(myThread, "t1");
		Thread t2 = new Thread(myThread, "t2");
		Thread t3 = new Thread(myThread, "t3");
		Thread t4 = new Thread(myThread, "t4");
		Thread t5 = new Thread(myThread, "t5");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
}
示例总结

当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码体内容:拿不到锁,这个线程就会不断的尝试获得这个锁,直到拿到为止。而且是多个线程同时取竞争这把锁。(也就是会有所竞争的问题)。

多个线程多个锁

多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。

示例代码
package cn.itcast_01;

/*
 * 打印结果
 * 		tag a,set num over!
 * 		tag a, num = 100
 * 		tag b,set num over!
 * 		tag b, num = 200
 */
public class MultiThread {
    
    

    private static int num = 0;

    // static
    // 在静态方法上加synchronized关键字,表示类级别的锁(MultiThread.class)
    public static synchronized void printNum(String tag) {
    
    
        try {
    
    
            if (tag.equals("a")) {
    
    
                num = 100;
                System.out.println("tag a,set num over!");
                Thread.sleep(1000);
            } else {
    
    
                num = 200;
                System.out.println("tag b,set num over!");
            }
            System.out.println("tag " + tag + ", num = " + num);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    // 注意观察run方法输出顺序
    public static void main(String[] args) {
    
    
        // 两个不同的对象
        final MultiThread m1 = new MultiThread();
        final MultiThread m2 = new MultiThread();

        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                m1.printNum("a");
            }
        });
        Thread t2 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                m2.printNum("b");
            }
        });

        t1.start();
        t2.start();
    }
} 
示例总结

关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以示例代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。

有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,类级别的锁(独占.class类)。

对象锁的同步和异步

同步:synchronized

同步的概念就是共享,我们要牢牢记住"共享"这两个字,如果不是共享的资源,就没有必要进行同步。

异步:asynchronized

异步的概念就是独立,相互之间不受到任何限制。就好像我们学习http的时候,在页面发起的ajax请求,我们还可以继续浏览或操作页面的内容,二者之间没有任何关系。

同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性:

  • 原子性(同步)
  • 可见性
示例代码
package cn.itcast_02;

/*
 * 对象锁的同步和异步问题
 */
public class Myobject {
    
    

    public synchronized void method1() {
    
    
        try {
    
    
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    // synchronized
    // 不加synchronized t1 t2同时进行
    // 加了synchronized,执行完t1,再执行t2
    public synchronized void method2() {
    
    
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
    
    
        final Myobject mo = new Myobject();
        /*
         * 分析:
         * 	t1线程先持有Object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法。
         * 	t1线程先持有Object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步。
         */
        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                mo.method1();
            }
        }, "t1");
        Thread t2 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                mo.method2();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
示例总结
  • A线程先持有Object对象的Lock锁,B线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步。
  • A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用对象中的非synchronized修饰的方法。

脏读

对于对象的同步和异步的方法,我们在设计自己的程序的时候,一定要考虑问题的整体,不然就会出现数据不一致的错误,很经典的错误就是脏读(dirtyRead

示例代码
package cn.itcast_02;

/*
 * 业务整体需要使用完整的synchronized,保证业务的原子性
 */
public class DirtyRead {
    
    

    private String username = "bjsxt";
    private String password = "123";

    public synchronized void setValue(String username, String password) {
    
    
        this.username = username;
        try {
    
    
            Thread.sleep(2000);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

        this.password = password;
        System.out.println("setValue最终结果: username = " + username + " , password = " + password);
    }

    // 在getValue()方法上加synchronized关键字,保持同步
    // 不加synchronized关键字,会出现脏读
    public synchronized void getValue() {
    
    
        System.out.println("getValue方法得到: username = " + this.username + " , password = " + this.password);
    }

    public static void main(String[] args) throws Exception {
    
    
        final DirtyRead dr = new DirtyRead();
        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                dr.setValue("zs", "456");
            }
        });
        t1.start();
        Thread.sleep(1000);
        dr.getValue();

    }
}
示例总结

在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue方法同时加锁synchronized同步关键字,保证业务(service)的原子性,不然会出现业务错误(也从侧面保证业务的一致性)

synchronized其他概念

关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到了一个对象的锁后,再次请求此对象时可以再次得到该对象的锁。

示例代码一
package cn.itcast_03;

/*
 * synchronized锁重入
 */
public class SyncDubbo1 {
    
    

    public synchronized void method1() {
    
    
        System.out.println("method1....");
        method2();
    }

    public synchronized void method2() {
    
    
        System.out.println("method2....");
        method3();
    }

    public synchronized void method3() {
    
    
        System.out.println("method3....");
    }

    public static void main(String[] args) {
    
    
        final SyncDubbo1 sd = new SyncDubbo1();
        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                sd.method1();
            }
        });
        t1.start();
    }
}
示例代码二
package cn.itcast_03;

/*
 * synchronized锁重入
 */
public class SyncDubbo2 {
    
    

    static class Main {
    
    
        public int i = 10;

        public synchronized void operationSup() {
    
    
            try {
    
    
                i--;
                System.out.println("Main print i = " + i);
                Thread.sleep(100);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    static class Sub extends Main {
    
    
        public synchronized void operationSub() {
    
    
            try {
    
    
                while (i > 0) {
    
    
                    i--;
                    System.out.println("Sub print i = " + i);
                    Thread.sleep(100);
                    this.operationSup();
                }
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
    
    

        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                Sub sub = new Sub();
                sub.operationSub();
                ;
            }
        });
        t1.start();

    }
}

出现异常,锁自动释放

示例代码
package cn.itcast_03;

/*
 * synchronized锁重入
 */
public class SyncDubbo2 {
    
    

    static class Main {
    
    
        public int i = 10;

        public synchronized void operationSup() {
    
    
            try {
    
    
                i--;
                System.out.println("Main print i = " + i);
                Thread.sleep(100);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    static class Sub extends Main {
    
    
        public synchronized void operationSub() {
    
    
            try {
    
    
                while (i > 0) {
    
    
                    i--;
                    System.out.println("Sub print i = " + i);
                    Thread.sleep(100);
                    this.operationSup();
                }
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }/*
     * synchronized异常
     */

    public class SyncException {
    
    

        private int i = 0;

        public synchronized void operation() {
    
    
            while (true) {
    
    
                try {
    
    
                    i++;
                    Thread.sleep(200);
                    System.out.println(Thread.currentThread().getName() + " : i = " + i);
                    if (i == 10) {
    
    
                        Integer.parseInt("a");// 在此处发生异常:NumberFormatException
                        // throw new RuntimeException();
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                    System.out.println(" log info i = " + i);
                    // throw new RuntimeException();//抛出运行时异常,让程序停止
                    // continue;
                }
            }
        }

        public /*static*/ void main(String[] args) {
    
    
            final SyncException se = new SyncException();
            Thread t1 = new Thread(new Runnable() {
    
    

                public void run() {
    
    
                    se.operation();
                }
            }, "t1");
            t1.start();
        }
    }

    public static void main(String[] args) {
    
    

        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                Sub sub = new Sub();
                sub.operationSub();
                ;
            }
        });
        t1.start();

    }
}

说明:对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对你的应用程序业务逻辑产生严重的错误,比如你现在执行一个队列任务,很多对象都去在等待第一个对象正确执行完毕再去释放锁,但是第一个对象由于异常的出现,导致业务逻辑没有正常执行完毕,就释放了锁,那么可想而知后续的对象执行的都是错误的逻辑。所以这一点一定要引起注意,在编写代码的时候,一定要考虑周全。

synchronized代码块

使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况下可以使用synchronized代码块取优化代码执行时间,也就是通常所说的减小锁的粒度。

示例代码

synchronized可以使用任意的Object进行加锁,用法比较灵活。

示例代码
package cn.itcast_03;

/*
 * 使用synchronized代码块加锁,比较灵活
 */
class ObjectLock {
    
    

    public void method1() {
    
    
        synchronized (this) {
    
    // 对象锁
            try {
    
    
                System.out.println("do method1...");
                Thread.sleep(2000);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public void method2() {
    
    
        synchronized (ObjectLock.class) {
    
    // 类锁
            try {
    
    
                System.out.println("do method2...");
                Thread.sleep(2000);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    private Object lock = new Object();

    public void method3() {
    
    // 任何对象锁
        synchronized (lock) {
    
    
            try {
    
    
                System.out.println("do method3...");
                Thread.sleep(2000);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
    
    
        final ObjectLock obLock = new ObjectLock();
        Thread t1 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                obLock.method1();
            }
        });
        Thread t2 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                obLock.method2();
            }
        });
        Thread t3 = new Thread(new Runnable() {
    
    

            public void run() {
    
    
                obLock.method3();
            }
        });

        t1.start();
        t2.start();
        t3.start();
    }
}

另外特别注意一个问题,就是不要使用String的常量加锁,会出现死循环问题。

示例代码
/*
 * synchronized代码块对字符串的锁,注意String常量池的缓存功能
 */
public class StringLock {
    
    

	public void method() {
    
    
		// new String("字符串常量") 可以使用此方式来加锁
		// 不要使用字符串常量加锁
		synchronized ("字符串常量") {
    
    
			try {
    
    
				while (true) {
    
    
					System.out.println("当前线程:" + Thread.currentThread().getName() + "开始");
					Thread.sleep(1000);
					System.out.println("当前线程:" + Thread.currentThread().getName() + "结束");
				}
			} catch (Exception e) {
    
    
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
    
    

		final StringLock stringLock = new StringLock();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				stringLock.method();
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				stringLock.method();
			}
		}, "t2");
		t1.start();
		t2.start();
	}
}

锁对象的改变问题,当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。

示例代码一
package cn.itcast_06;

/*
 * 同一对象属性的修改不会影响锁的情况
 */
public class ModifyLock {
    
    

	private String name;
	private int age;

	public String getName() {
    
    
		return name;
	}

	public void setName(String name) {
    
    
		this.name = name;
	}

	public int getAge() {
    
    
		return age;
	}

	public void setAge(int age) {
    
    
		this.age = age;
	}

	public synchronized void changeAttributte(String name, int age) {
    
    
		try {
    
    
			System.out.println("当前线程:" + Thread.currentThread().getName() + "开始");
			this.setName(name);
			this.setAge(age);
			System.out.println(
					"当前线程:" + Thread.currentThread().getName() + "修改对象内容为:" + this.getName() + " , " + this.getAge());
			Thread.sleep(2000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "结束");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
    
    
		final ModifyLock modifyLock = new ModifyLock();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				modifyLock.changeAttributte("张三", 20);
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				modifyLock.changeAttributte("李四", 21);
			}
		}, "t2");

		t1.start();
		try {
    
    
			Thread.sleep(100);
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}

		t2.start();
	}
}
示例代码二
package cn.itcast_06;
/*
 * 锁对象的改变问题
 */
public class ChangeLock {
    
    

	private String lock = "lock";

	private void method() {
    
    
		synchronized (lock) {
    
    
			try {
    
    
				System.out.println("当前线程:" + Thread.currentThread().getName() + "开始");
				lock = "change lock";// 不要修改锁
				Thread.sleep(2000);
				System.out.println("当前线程:" + Thread.currentThread().getName() + "结束");
			} catch (Exception e) {
    
    
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
    
    

		final ChangeLock changeLock = new ChangeLock();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				changeLock.method();
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				changeLock.method();
			}
		}, "t2");

		t1.start();
		try {
    
    
			Thread.sleep(100);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
		t2.start();

	}
}

死锁问题

volatile关键字概念

volatile概念:volatile关键字的主要作用是使变量在多个线程间可见。

示例代码
public class RunThread extends Thread {
    
    

	// volatile
	private volatile boolean isRunning = true;

	private void setRunning(boolean isRunning) {
    
    
		this.isRunning = isRunning;
	}

	public void run() {
    
    
		System.out.println("进入run方法...");
		while (isRunning == true) {
    
    
			// ..
		}
		System.out.println("线程停止");
	}

	public static void main(String[] args) throws InterruptedException {
    
    
		RunThread rt = new RunThread();
		rt.start();
		Thread.sleep(3000);
		rt.setRunning(false);
		System.out.println("isRunning的值已经被设置了false");
		Thread.sleep(1000);
		System.out.println(rt.isRunning);
	}
}

示例总结

在java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝,当线程执行时,它在自己的工作内存区中操作这些变量。为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到它自己所在的工作内存区中,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。

一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、锁定(lock)、解锁(unlock)。

而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock),每个操作都是原子的。

volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不去线程工作内存区里去读取,从而实现了多个线程间的变量可见,也就是满足线程安全的可见性。

volatile关键字线程执行流程图

在这里插入图片描述

volatile关键字的非原子性

volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码就大量使用volatile,可见netty性能一定是非常不错的。)这里需要注意:一般volatile用于只针对于多个线程间可见的变量操作,并不能代替synchronized的同步功能。

示例代码
import java.util.concurrent.atomic.AtomicInteger;
/*
 * volatile关键字不具备synchronized关键字的原子性(同步)
 * AtomicInteger类 可以用原子方式更新的 int 值。
 */
public class VolatileNoAtomic extends Thread {
    
    

	// private static volatile int count;
	private static AtomicInteger count = new AtomicInteger(0);

	private static void addCount() {
    
    
		for (int i = 0; i < 1000; i++) {
    
    
			// count++;
			count.incrementAndGet();
		}
		System.out.println(count);
	}

	public void run() {
    
    
		addCount();
	}

	public static void main(String[] args) {
    
    
		VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
		for (int i = 0; i < 10; i++) {
    
    
			arr[i] = new VolatileNoAtomic();
		}
		for (int i = 0; i < 10; i++) {
    
    
			arr[i].start();
		}
	}
}
示例总结

volatile关键字只具有可见性,没有原子性。要实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性)。

示例代码
package cn.itcast_07;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicUse {
    
    

	private static AtomicInteger count = new AtomicInteger(0);

	// 多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性。
	// synchronized
	public synchronized int multiAdd() {
    
    
		try {
    
    
			Thread.sleep(100);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
		count.addAndGet(1);
		count.addAndGet(2);
		count.addAndGet(3);
		count.addAndGet(4);// +10
		return count.get();
	}

	public static void main(String[] args) {
    
    
		final AtomicUse au = new AtomicUse();
		List<Thread> ts = new ArrayList<Thread>();
		for (int i = 0; i < 100; i++) {
    
    
			ts.add(new Thread(new Runnable() {
    
    
				@Override
				public void run() {
    
    
					System.out.println(au.multiAdd());
				}
			}));
		}

		for (Thread t : ts) {
    
    
			t.start();
		}
	}
}

线程之间通信

线程通信概念

线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控与监督。

使用wait/notify方法实现线程间的通信。(注意这两个方法都是object的类的方法,换句话说java为所有的对象都提供了这两个方法)

  1. wait和notify必须配合synchronized关键字使用
  2. wait方法释放锁,notify方法不释放锁
示例代码一(没有使用wait和notify方法的实现)
package cn.itcast_01;
import java.util.ArrayList;
import java.util.List;
public class ListAdd1 {
    
    
	private volatile static List list = new ArrayList();

	public void add() {
    
    
		list.add("bjsxt");
	}

	public int size() {
    
    
		return list.size();
	}

	public static void main(String[] args) {
    
    
		final ListAdd1 list1 = new ListAdd1();
		Thread t1 = new Thread(new Runnable() {
    
    
			public void run() {
    
    
				try {
    
    
					for (int i = 0; i < 10; i++) {
    
    
						list1.add();
						System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
						Thread.sleep(500);
					}
				} catch (Exception e) {
    
    
					e.printStackTrace();
				}
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				while (true) {
    
    
					if (list1.size() == 5) {
    
    
						System.out.println("当前线程收到通知:" + Thread.currentThread().getName() + " list size = 5 线程停止..");
						throw new RuntimeException();
					}
				}
			}
		}, "t2");

		t1.start();
		t2.start();
	}
}
示例代码二(线程间通信实现)
package cn.itcast_01;
import java.util.ArrayList;
import java.util.List;
/*
 * 	wait()方法和notify()方法
 * 		wait():释放锁
 * 		notify():不释放锁
 */
public class ListAdd2 {
    
    
	private volatile static List list = new ArrayList();

	public void add() {
    
    
		list.add("bjsxt");
	}

	public int size() {
    
    
		return list.size();
	}

	public static void main(String[] args) {
    
    

		final ListAdd2 list2 = new ListAdd2();
		// 实例化出来一个lock
		// 当使用wait和notify的时候,一定要配合着synchronized关键字去使用
		final Object lock = new Object();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					synchronized (lock) {
    
    
						System.out.println("t1启动..");
						for (int i = 0; i < 10; i++) {
    
    
							list2.add();
							System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
							Thread.sleep(500);
							if (list2.size() == 5) {
    
    
								System.out.println("已经发出通知..");
								lock.notify();
							}
						}
					}
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}

			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				synchronized (lock) {
    
    
					System.out.println("t2启动..");
					if (list2.size() != 5) {
    
    
						try {
    
    
							lock.wait();
						} catch (InterruptedException e) {
    
    
							e.printStackTrace();
						}
					}
					System.out.println("当前线程:" + Thread.currentThread().getName() + "收到通知线程停止..");
					throw new RuntimeException();
				}
			}
		}, "t2");
		t2.start();
		t1.start();
	}
}

弊端:notify()方法不释放锁。如何解决呢?用java.util.concurrent包下的工具类 CountDownLatch可以实现线程间的实时通信。代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/*
 * CountDownLatch:可以实现线程间的实时通信
 */
public class ListAdd3 {
    
    
	private volatile static List list = new ArrayList();

	public void add() {
    
    
		list.add("bjsxt");
	}

	public int size() {
    
    
		return list.size();
	}

	public static void main(String[] args) {
    
    

		final ListAdd3 list2 = new ListAdd3();
		// 参数"1":代表发几次countDownLatch.countDown();
		final CountDownLatch countDownLatch = new CountDownLatch(1);
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					// synchronized (lock) {
    
    
					System.out.println("t1启动..");
					for (int i = 0; i < 10; i++) {
    
    
						list2.add();
						System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
						Thread.sleep(500);
						if (list2.size() == 5) {
    
    
							System.out.println("已经发出通知..");
							// lock.notify();
							countDownLatch.countDown();
						}
						// }
					}
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}

			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				// synchronized (lock) {
    
    
				System.out.println("t2启动..");
				if (list2.size() != 5) {
    
    
					try {
    
    
						// lock.wait();
						countDownLatch.await();
					} catch (InterruptedException e) {
    
    
						e.printStackTrace();
					}
					// }
					System.out.println("当前线程:" + Thread.currentThread().getName() + "收到通知线程停止..");
					throw new RuntimeException();
				}
			}
		}, "t2");
		t2.start();
		t1.start();
	}
}

使用wait/notify模拟Queue

  • BlockingQueue:顾名思义,首先它是一个队列,并且支持阻塞的机制,阻塞的放入和得到数据。我们要实现LinkedBlockingQueue下面两个简单的方法 put和take。
  • put(anObject):把anObject加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续。
  • take:取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入。
LinkedBlockingQueue方法
//构造方法
public LinkedBlockingQueue()
    //LinkedBlockingQueue构造的时候若没有指定大小,则默认大小为Integer.MAX_VALUE,当然也可以在构造函数的参数中指定大小。
    //LinkedBlockingQueue不接受null
public LinkedBlockingQueue(int capacity)//创建一个具有给定(固定)容量的 LinkedBlockingQueue。 
//添加元素的方法:
    boolean add(E e)//将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。当使用有容量限制的队列时,通常首选 offer。
    public void put(E e)//将指定元素插入到此队列的尾部,如有必要,则等待空间变得可用。
    public boolean offer(E e)//将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量),
                               //在成功时返回 true,如果此队列已满,则返回 false。当使用有容量限制的队列时,
                               //此方法通常要优于 add 方法,后者可能无法插入元素,而只是抛出一个异常。 
//获取元素的方法:
public E poll()//获取并移除此队列的头,如果此队列为空,则返回 null。
public boolean remove(Object o)//从此队列移除指定元素的单个实例(如果存在)。若队列为空,抛出NoSuchElementException异常。
public E take() throws InterruptedException://获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
示例代码
package cn.itcast_02;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/*
 * 模拟Queue
 */
public class MyQueue {
    
    

	private final LinkedList<Object> list = new LinkedList<>();

	private final AtomicInteger count = new AtomicInteger(0);

	private final int maxSize;
	private final int minSize = 0;

	public MyQueue(int maxSize) {
    
    
		this.maxSize = maxSize;
	}

	private final Object lock = new Object();

	public void put(Object obj) {
    
    
		synchronized (lock) {
    
    
			while (count.get() == this.maxSize) {
    
    
				try {
    
    
					lock.wait();
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
			// 加入元素
			list.add(obj);
			// 计数器累加
			count.incrementAndGet();
			// 通知另外一个线程(唤醒)
			lock.notify();
			System.out.println(" 元素 " + obj + " 被添加 ");
		}
	}

	public Object take() {
    
    
		Object temp = null;
		synchronized (lock) {
    
    
			while (count.get() == this.minSize) {
    
    
				try {
    
    
					lock.wait();
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
			// 做移除元素操作
			temp = list.removeFirst();
			// 计数器递减
			count.decrementAndGet();
			System.out.println(" 元素 " + temp + " 被消费 ");
			// 唤醒另外一个线程
			lock.notify();
		}
		return temp;
	}

	public int size() {
    
    
		return count.get();
	}

	public static void main(String[] args) throws InterruptedException {
    
    
		final MyQueue m = new MyQueue(5);
		m.put("a");
		m.put("b");
		m.put("c");
		m.put("d");
		m.put("e");
		System.out.println("当前元素个数:" + m.size());
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				m.put("h");
				m.put("i");
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					Thread.sleep(1000);
					Object t1 = m.take();
					 System.out.println("被取走的元素为:" + t1);
					Thread.sleep(1000);
					Object t2 = m.take();
					 System.out.println("被取走的元素为:" + t2);
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
		}, "t2");

		t1.start();
		Thread.sleep(2000);
		t2.start();
	}
}

ThreadLocal

ThreadLocal概念

线程局部变量,是一种多线程间并发访问变量的解决方案。与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全。

从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程安全解决方案,在高并发量或者竞争激烈的场景,使用ThreadLocal可以在一定程度上减少锁竞争。

示例代码
public class ConnThreadLocal {
    
    

	public static ThreadLocal<String> th = new ThreadLocal<String>();

	public void setTh(String value) {
    
    
		th.set(value);
	}

	public void getTh() {
    
    
		System.out.println(Thread.currentThread().getName() + ":" + this.th.get());
	}

	public static void main(String[] args) throws InterruptedException {
    
    

		final ConnThreadLocal ct = new ConnThreadLocal();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				ct.setTh("张三");
				ct.getTh();
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					Thread.sleep(1000);
					// ct.setTh("李四");
					ct.getTh();
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
		}, "t2");

		t1.start();
		t2.start();
	}
}

单例&多线程

单例模式,最常见的就是饥饿模式和懒汉模式,一个直接实例化对象,一个在调用方法时进行实例化对象。在多线程模式中,考虑到性能和线程安全问题,我们一般选择下面两种比较经典的单例模式,在性能提高的同时,又保证了线程的安全。

  • dubble check instance 为确保线程安全,需要两次check --懒汉模式
  • static inner class 静态内部类–饿汉模式
单例模式

解决的问题:保证一个类在内存中的对象唯一性。

比如:多线程读取一个配置文件时,建议配置文件封装成对象。会方便操作其中数据,又要保证多个程序读到的是同一个配置文件对象。

就需要该配置文件对象在内存中是唯一的。

如何保证对象唯一性呢?

思想:

  1. 不让其他程序创建该类对象。
  2. 在本类中创建一个本类对象。
  3. 对外提供方法,让其他程序获取这个对象。

步骤:

  1. 因为创建对象都需要构造函数初始化,只要将本类中的构造函数私有化,其他程序就无法再创建该类对象;
  2. 就在类中创建一个私有并静态的本类的对象;
  3. 定义一个方法,返回该对象,让其他程序可以通过方法就得到本类对象。(作用:可控)

代码体现:

  1. 私有化构造函数;
  2. 创建私有并静态的本类对象;
  3. 定义公有并静态的方法,返回该对象。
饿汉模式
class Single {
    
    
	// 1、私有化构造函数
	private Single() {
    
    
	}

	// 2、创建私有并静态的本类对象
	private static Single s = new Single();

	// 3、定义公有并静态的方法,返回该对象
	public static Single getInstance() {
    
    
		return s;
	}
}
懒汉模式

延迟加载方式,使用的时候才会去初始化该对象。

/**
 * 静态内部类做单例
 *
 */
class Single2 {
    
    
	private Single2() {
    
    

	}

	private static Single2 s = null;

	public static Single2 getInstance() {
    
    
		if (s == null) {
    
    
			s = new Single2();
		}
		return s;
	}
}

在多线程模式中,考虑到性能和线程安全问题,我们一般会选择下面两种比较经典的单例模式,在性能提高的同时,又保证了线程的安全.

  • static inner Class
  • dubble check instance

饿汉模式 – static inner Class – 内部静态类的形式

public class InnerSingleton {
    
    

	private static class Singletion {
    
    
		private static Singletion single = new Singletion();
	}

	public static Singletion getInstance() {
    
    
		return Singletion.single;
	}
}

懒汉模式 – dubble check instance – 为确保线程安全,需要两次check

public class DubbleSingleton {
    
    

	private static DubbleSingleton ds;

	public static DubbleSingleton getDs() {
    
    
		if (ds == null) {
    
    
			try {
    
    
				// 模拟初始化对象的准备时间...
				Thread.sleep(3000);
			} catch (InterruptedException e) {
    
    
				e.printStackTrace();
			}
			synchronized (DubbleSingleton.class) {
    
    
				if (ds == null) {
    
    
				    //初始化有可能耗时比较长,导致N个线程到达【等待位置】,如果这里不第二次检查ds,就会生成N个新实例对象。
					ds = new DubbleSingleton();
				}
			}
		}
		return ds;
	}

	public static void main(String[] args) {
    
    
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		}, "t2");
		Thread t3 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				System.out.println(DubbleSingleton.getDs().hashCode());
			}
		}, "t3");

		t1.start();
		t2.start();
		t3.start();
	}
}

同步类容器

同步类容器都是线程安全的,例如Vector、HashTable等。但是在某些场景下可能需要加锁来保护复合操作,例如:迭代(反复访问元素,遍历完容器中所有的元素),跳转(根据指定的顺序找到当前元素的下一个元素),以及条件运算。这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的就是java.util.ConcurrentModificationException,原因是当容器迭代的工程中,并发地修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。

传统的同步类容器(Vector,HashTable),这些容器的同步功能都是有JDK的Collections.synchronized***等工厂方法去创建实现的。其底层的机制无非就是用传统的synchronized关键字对每一个公用的方法都进行同步,使得每次只能有一个线程访问容器状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须要有足够好的性能。

示例代码
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

/*
 * 多线程使用Vector或者Hashtable的示例(简单线程同步的问题)
 */
public class Tickets {
    
    

	public static void main(String[] args) {
    
    
		// 初始化火车票池并添加火车票:避免线程同步可采用Vector替代ArrayList Hashtable替代HashMap
		final Vector<String> tickets = new Vector<String>();

		// HashMap是线程不安全的, 但在此处是线程安全的,因为被synchronizedMap包裹
		// Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

		for (int i = 0; i <= 1000; i++) {
    
    
			tickets.add("火车票" + i);
		}

		// java.util.ConcurrentModificationException
		// for (Iterator iterator = tickets.iterator(); iterator.hasNext();) {
    
    
		// String string = (String) iterator.next();
		// tickets.remove(20);
		// }

		for (int i = 1; i <= 10; i++) {
    
    
			new Thread("线程" + i) {
    
    
				public void run() {
    
    
					while (true) {
    
    
						if (tickets.isEmpty())
							break;
						System.out.println(Thread.currentThread().getName() + "---" + tickets.remove(0));
					}
				}
			}.start();
		}
	}
}
//=========================================================================================================
Vector里面的add()方法源码-----都是加synchronized修饰,保证同步
public synchronized boolean add(E e) {
    
    
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
public synchronized E get(int index) {
    
    
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

并发类容器

JDK1.5之后提供了多种并发类容器来取代同步类容器以提高性能。同步类容器的状态都是串行化的。它们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量。

并发类容器时专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的Hashtable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持。以及使用了CopyOnWriteArrayList代替Vector,并发的CopyOnWriteArraySet,以及并发的QueueConcurrentLinkedQueueLinkedBlockingQueue,前者是高性能的队列,后者是以阻塞形式的队列,具体实现Queue还有很多,例如ArrayBlockingQueuePriorityBlockingQueueSynchronousQueue等。

ConcurrentMap

ConcurrentMap接口下有两个重要的实现:

  • ConcurrentHashMap
  • ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)

并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时降低了对读一致性的要求(这点好像CAP理论啊 O(∩_∩)O)。ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响,无论对于Java并发编程的学习还是Java内存模型的理解,ConcurrentHashMap的设计以及源码都值得非常仔细的阅读与揣摩。

ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,他们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发的进行。把一个整体分成了16个小段(Sgment)。也就是最高支持16个线程的并发修改操作。这也在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class UseConcurrentMap {
    
    

	public static void main(String[] args) {
    
    
		ConcurrentHashMap<String, Object> chw = new ConcurrentHashMap<String, Object>();

		chw.put("k1", "v1");
		chw.put("k2", "v2");
		chw.put("k3", "v3");
		// chw.putIfAbsent("k3", "vvvvv");// 如果有相同的键,就不添加元素
		chw.putIfAbsent("k4", "vvvvv");// 如果键不相同,就添加元素

		// System.out.println(chw.get("k2"));// v2
		// System.out.println(chw.size());// 3

		for (Map.Entry<String, Object> me : chw.entrySet()) {
    
    
			System.out.println("key:" + me.getKey() + "value:" + me.getValue());
		}
	}
}

Copy-On-Write容器

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。

JDK里的COW容器有两种:CopyOnWriteArrayListCopyOnWriteArraySet

COW容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWritering容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。读多写少的时候,用此容器。

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。如下代码

package com.ifeve.book;
 
import java.util.Map;
 
import com.ifeve.book.forkjoin.CopyOnWriteMap;
 
/**
 * 黑名单服务
 *
 * @author fangtengfei
 *
 */
public class BlackListServiceImpl {
    
    
 
    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
            1000);
 
    public static boolean isBlackList(String id) {
    
    
        return blackListMap.get(id) == null ? false : true;
    }
 
    public static void addBlackList(String id) {
    
    
        blackListMap.put(id, Boolean.TRUE);
    }
 
    /**
     * 批量添加黑名单
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
    
    
        blackListMap.putAll(ids);
    }
}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题 。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

数据一致性问题 。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

public class UseCopyOnWrite {
    
    

	public static void main(String[] args) {
    
    
		CopyOnWriteArrayList<String> cwal = new CopyOnWriteArrayList<String>();
		CopyOnWriteArraySet<String> cwas = new CopyOnWriteArraySet<String>();
	}
}

并发Queue

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。
在这里插入图片描述

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个适用于高并发场景下的队列(一个基于链接节点的无界无阻塞线程安全队列),通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链表节点的无界线程安全队列,该队列的元素遵循先进先出的原则,头是最先加入的,尾是最近加入的,该队列不允许null元素。

ConcurrentLinkedQueue的重要方法:

  • add()和offer()方法都是加入元素的方法,(在ConcurrentLinkedQueue中,这两个方法没有任何区别)
  • poll()和peek()方法都是头元素节点,区别在于前者会删除元素,后者不会。
BlockingQueue接口

ArrayBlockingQueue基于数组的有界阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合非常适合使用。

//构造方法
public ArrayBlockingQueue(int capacity)//创建一个带有给定的(固定)容量和默认访问策略的 ArrayBlockingQueue。 
                                         //参数:capacity - 此队列的容量方法
public boolean add(E e)//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),
                         //在成功时返回 true,如果此队列已满,则抛出 IllegalStateException。
public boolean offer(E e)//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,
                           //如果此队列已满,则返回 false。此方法通常要优于 add(E) 方法,
                           //后者可能无法插入元素,而只是抛出一个异常。 
public void put(E e) throws InterruptedException://将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间。
public boolean offer(E e,long timeout,TimeUnit unit) throws InterruptedException://将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。
public boolean remove(Object o)//从此队列中移除指定元素的单个实例(如果存在)。更确切地讲,
                                 //如果此队列包含一个或多个满足 o.equals(e) 的元素 e,则移除该元素。
                                 //如果此队列包含指定的元素(或者此队列由于调用而发生更改),则返回 true。 
public E poll()//从接口 Queue复制的描述获取并移除此队列的头,如果此队列为空,则返回 null。   
public E poll(long timeout,TimeUnit unit) throws InterruptedException://从接口 BlockingQueue 复制的描述获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。
public E take() throws InterruptedException://从接口 BlockingQueue 复制的描述获取并移除此队列的头部,
                                             //在元素变得可用之前一直等待(如果有必要)。                   

LinkedBlockingQueue基于链表的无界阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。它是一个无界队列。

//构造方法
public LinkedBlockingQueue()//创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue。
public LinkedBlockingQueue(int capacity)//创建一个具有给定(固定)容量的 LinkedBlockingQueue。
//方法同上

PriorityBlockingQueue基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,它也是一个无界队列。

//构造方法
public PriorityBlockingQueue()//用默认的初始容量 (11) 创建一个 PriorityBlockingQueue,并根据元素的自然顺序对其元素进行排序。 

DelayQueue一个无界阻塞队列,带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素,DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲连接数的关闭等等。

//构造方法    
public DelayQueue()//创建一个最初为空的新 DelayQueue。 

SynchronousQueue一种没有缓冲的阻塞队列,生产者产生的数据直接会被消费者获取并消费。不允许添加元素。

//构造方法
public SynchronousQueue()//创建一个具有非公平访问策略的 SynchronousQueue。

BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
在这里插入图片描述

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class UseQueue {
    
    
	public static void main(String[] args) throws Exception {
    
    

		// 高性能无阻塞无界队列:ConcurrentLinkedQueue
		ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<String>();
		q.offer("a");
		q.offer("b");
		q.offer("c");
		q.offer("d");
		System.out.println(q.poll());// 取头元素节点,然后删除
		System.out.println(q.size());
		System.out.println(q.peek());// 取头元素节点,不删除
		System.out.println(q.size());

		// 有界阻塞队列
		ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5);
		array.put("a");
		array.put("b");
		array.add("c");
		array.add("d");
		array.add("e");
		// java.lang.IllegalStateException: Queue full
		// array.add("f");
		// System.out.println(array.offer("g", 2, TimeUnit.SECONDS));

		// 无界阻塞队列

		LinkedBlockingDeque<String> q1 = new LinkedBlockingDeque<String>(5);
		q1.offer("a");
		q1.offer("b");
		q1.offer("c");
		q1.offer("d");
		q1.offer("e");
		q1.offer("f");
		q1.offer("g");
		// System.out.println(q1.size());
		List<String> list = new ArrayList<String>();
		// drainTo()方法:从队列中取3个元素,放到集合中
		System.out.println(q1.drainTo(list, 3));
		System.out.println(list.size());
		for (String string : list) {
    
    
			System.out.println(string);
		}

		SynchronousQueue<String> sq = new SynchronousQueue<>();
		// java.lang.IllegalStateException: Queue full
		// 不允许添加元素
		sq.add("a");
		
		//这个队列没有任何容量,不装任何元素,
		final SynchronousQueue<String> sq1 = new SynchronousQueue<>();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					System.out.println(sq1.take());//取元素
				} catch (Exception e) {
    
    
					e.printStackTrace();
				}
			}
		});
		t1.start();
		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				// 可以添加元素,但是前提是得调用take()方法
				sq1.add("添加元素...");
			}
		});
		t2.start();
	}
}
import java.util.concurrent.PriorityBlockingQueue;

public class UsePriorityBlockingQueue {
    
    

	public static void main(String[] args) throws InterruptedException {
    
    
		PriorityBlockingQueue<Task> p = new PriorityBlockingQueue<Task>();

		Task t1 = new Task();
		t1.setId(3);
		t1.setName("任务1");

		Task t2 = new Task();
		t2.setId(4);
		t2.setName("任务2");

		Task t3 = new Task();
		t3.setId(1);
		t3.setName("任务3");

		p.offer(t1);
		p.offer(t2);
		p.offer(t3);
		// for (Task task : p) {
    
    
		// System.out.println(task.getName());
		// }

		System.out.println("容器:" + p);
		// 调用take()方法才会去排序
		System.out.println(p.take().getId());
		System.out.println("容器:" + p);
		System.out.println(p.take().getId());
		System.out.println(p.take().getId());
	}
}
//=========================================================
public class Task implements Comparable<Task> {
    
    

	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;
	}

	@Override
	public int compareTo(Task task) {
    
    

		return this.id > task.id ? 1 : (this.id < task.id ? -1 : 0);
	}

	@Override
	public String toString() {
    
    
		return "Task [id=" + id + ", name=" + name + "]";
	}
}

多线程的设计模式

并行设计模式属于设计优化的一部分,它是对一些常用的多线程结构的总结和抽象。

与串行程序相比,并行程序的结构通常更为复杂,因此合理的使用并行模式在多线程开发中更具有意义,在这里主要介绍Future、Master-Worker和生产者-消费者模型。

Future模式

Future模式有点类似于商品订单。比如在网购时,当看中某一件商品时,就可以提交订单,当订单处理完成后,在家里等待送货上门即可。或者说更形象的我们发送Ajax请求的时候,页面是异步的进行后台处理,用户无需一直等待请求的结果,可以继续浏览或操作其他内容。
在这里插入图片描述
示例代码

public class Main {
    
    

	public static void main(String[] args) {
    
    
		FutureClient fc = new FutureClient();
		Data data = fc.request("请求参数");
		System.out.println("请求发送成功!");
		System.out.println("做其他的事情...");
		String result = data.getRequest();
		System.out.println(result);
	}
}
//=================================================
public class FutureClient {
    
    

	public Data request(final String queryStr) {
    
    
		// 1、我想要一个代理对象(Data接口的实现类)先返回给发送请求的客户端,告诉它请求已经接收到,可以做其他的事情
		final FutureData futureData = new FutureData();
		// 2、启动一个新的线程,去加载真实的数据,传递给这个代理对象
		new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    
				// 3、这个新的线程可以去慢慢的加载真实对象,然后传递给代理对象
				RealData realData = new RealData(queryStr);
				futureData.setRealData(realData);
			}
		}).start();
		return futureData;
	}
}
//===========================================================================================
public class FutureData implements Data {
    
    

	private RealData realData;
	private boolean isReady = false;

	public synchronized void setRealData(RealData realData) {
    
    
		// 如果已经装载完毕了,就直接返回
		if (isReady) {
    
    
			return;
		}
		// 如果没装载,进行装载真实对象
		this.realData = realData;
		isReady = true;
		// 进行通知
		notify();
	}

	@Override
	public synchronized String getRequest() {
    
    
		// 如果没装载好,程序就一直处于阻塞状态
		while (!isReady) {
    
    
			try {
    
    
				wait();
			} catch (InterruptedException e) {
    
    
				e.printStackTrace();
			}
		}
		// 装载好直接获取数据即可
		return this.realData.getRequest();
	}
}
//=======================================================================
public class RealData implements Data {
    
    

	private String result;

	public RealData(String queryStr) {
    
    
		System.out.println("根据" + queryStr + "进行查询,这是一个很耗时的操作...");
		try {
    
    
			Thread.sleep(5000);
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}
		System.out.println("操作完毕,获取结果");
		result = "查询结果";
	}

	@Override
	public String getRequest() {
    
    
		return result;
	}
}
//=================================================================================
public interface Data {
    
    
	
	String getRequest();
}
Master-Worker模式

Master-Worker模式是常用的并行模式。它的核心思想是系统由两类进程协作工作:Master进程和Worker进程。Master负责接收和分配任务,Worker负责处理子任务。当各个Worker子进程处理完成后,会将结果返回给Master,由Master做归纳和总结。其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。
在这里插入图片描述
在这里插入图片描述

课堂目录

  • JDK多任务执行框架
  • Concurrent.util工具类详细讲解和使用
  • (重入锁、读写锁使用)锁的高级深化

Executor框架

为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效的进行线程控制。它们都在java.util.concurrent包中,是JDK并发包的核心。其中有一个比较重要的类:Executors,它扮演着线程工厂的角色,我们通过Executors可以创建特定功能的线程池。

Executors创建线程池方法

newFixedThreadPool()方法

该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中等待有空闲的线程去执行。

public static ExecutorService newFixedThreadPool(int nThreads) //创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
newSingleThreadExecutor()方法

创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。

public static ExecutorService newSingleThreadExecutor()//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
newCachedThreadPool()方法

返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60s后自动回收。

public static ExecutorService newCachedThreadPool()//创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们
newScheduledThreadPool()方法

该方法返回一个SchededExecutorService对象,但该线程池可以指定线程的数量。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public class UseExecutors {
    
    
	public static void main(String[] args) {
    
    
		ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
	}
}
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

class Temp extends Thread {
    
    
	public void run() {
    
    
		System.out.println("run");
	}
}

public class ScheduledJob {
    
    

	public static void main(String args[]) throws Exception {
    
    

		Temp command = new Temp();
		ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

		ScheduledFuture<?> scheduleTask = scheduler.scheduleWithFixedDelay(command, 5, 1, TimeUnit.SECONDS);
	}
}

//也可以使用Spring Scheduled Task定时任务机制实现。

自定义线程池

若Executors工厂类无法满足我们的需求,可以自己去创建自定义的线程池,其实Executors工厂类里面 的创建线程方法其内部实现均是用了ThreadPoolExecutor这个类,这个类可以自定义线程。构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory,
            RejectedExecutionHandler handler){
    
    ...}​​
参数
  • corePoolSize - 池中锁保存的线程数,包括空闲线程。
  • maximumPoolSize - 池中允许的最大线程数。
  • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
  • unit - keepAliveTime 参数的时间单位。
  • workQueue - 执行前用于保持任务的队列。此队列仅保持由execute()方法提交的Runnable任务。
  • threadFactory - 执行程序创建新线程时使用的工厂。
  • handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
抛出
  • IllegalArgumentException - 如果 corePoolSize 或 keepAliveTime 小于 0,或者 maximumPoolSize 小于等于 0,或者 corePoolSize 大于maximumPoolSize。
  • NullPointerException - 如果 workQueue、threadFactory 或 handler 为 null。

自定义线程池使用详解

这个构造方法对于队列是什么类型的比较关键

有界队列

在使用有界队列时:ArrayBlockingQueue。若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,若大于corePoolSize,则会将任务加入队列,若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。

无界队列

无界的任务队列时:LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而有没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

JDK拒绝策略
  • AbortPolicy:直接抛出异常组织系统正常工作
  • CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
  • DiscardOldestPolicy:丢弃最老的一个请求,尝试再次提交当前任务。
  • DiscardPolicy:丢弃无法处理的任务,不给予任何处理。
  • 如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口。
  • 使用有界队列自定义线程池代码示例
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class UseThreadPoolExecutor1 {
    
    

	public static void main(String[] args) {
    
    
		/*
		 * 在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,
		 * 若大于corePoolSize,则会将任务加入队列, 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,
		 * 若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
		 */
		ThreadPoolExecutor pool = new ThreadPoolExecutor(
				1, // corePoolSize
				2, // maximumPoolSize
				60, // keepAliveTime
				TimeUnit.SECONDS, // unit
				new ArrayBlockingQueue<Runnable>(3) // workQueue //指定一种队列 (有界队列)
				// new DiscardOldestPolicy()
				);

		MyTask mt1 = new MyTask(1, "任务1");
		MyTask mt2 = new MyTask(2, "任务2");
		MyTask mt3 = new MyTask(3, "任务3");
		MyTask mt4 = new MyTask(4, "任务4");
		MyTask mt5 = new MyTask(5, "任务5");
		MyTask mt6 = new MyTask(6, "任务6");

		pool.execute(mt1);
		pool.execute(mt2);
		pool.execute(mt3);
		pool.execute(mt4);
		pool.execute(mt5);
		// java.util.concurrent.RejectedExecutionException抛出异常
		//pool.execute(mt6);

		pool.shutdown();
	}
}

//======================================================================================================
public class MyTask implements Runnable {
    
    

	private int taskId;
	private String taskName;

	public MyTask(int taskId, String taskName) {
    
    
		this.taskId = taskId;
		this.taskName = taskName;
	}

	public int getTaskId() {
    
    
		return taskId;
	}

	public void setTaskId(int taskId) {
    
    
		this.taskId = taskId;
	}

	public String getTaskName() {
    
    
		return taskName;
	}

	public void setTaskName(String taskName) {
    
    
		this.taskName = taskName;
	}

	@Override
	public void run() {
    
    
		try {
    
    
			System.out.println("run taskId =" + this.taskId);
			Thread.sleep(5000);
			// System.out.println("end taskId =" + this.taskId);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}

	public String toString() {
    
    
		return Integer.toString(this.taskId);
	}
}
  • 使用无界队列自定义线程池代码示例
package cn.itcast_03;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class UseThreadPoolExecutor2 implements Runnable {
    
    

	private static AtomicInteger count = new AtomicInteger(0);

	@Override
	public void run() {
    
    
		try {
    
    
			int temp = count.incrementAndGet();
			System.out.println("任务" + temp);
			Thread.sleep(2000);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception {
    
    
		System.out.println(Runtime.getRuntime().availableProcessors());
		BlockingQueue<Runnable> queue =
				// new ArrayBlockingQueue<>(10);//使用有界队列
				new LinkedBlockingQueue<Runnable>();// 使用无界队列
		ExecutorService executor = new ThreadPoolExecutor(
				5, // corePoolSize
				10, // maximumPoolSize 指定无界队列的时候,此参数无意义
				120L, // keepAliveTime
				TimeUnit.SECONDS, // unit
				queue// workQueue
		);
		for (int i = 0; i < 20; i++) {
    
    
			executor.execute(new UseThreadPoolExecutor2());
		}
		Thread.sleep(1000);
		System.out.println("queue size:" + queue.size());
		Thread.sleep(2000);
	}
}
  • 自定义拒绝策略代码实现
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy;

public class UseThreadPoolExecutor1 {
    
    

	public static void main(String[] args) {
    
    
		/*
		 * 在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,
		 * 若大于corePoolSize,则会将任务加入队列, 
		 * 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,
		 * 若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
		 * 
		 */
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1, // coreSize
				2, // MaxSize
				60, // 60
				TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3) // 指定一种队列
				// new LinkedBlockingQueue<Runnable>() // (有界队列)
				// , new MyRejected()
				// , new DiscardOldestPolicy()// 丢弃最老的任务,尝试再次提交的任务
		);

		MyTask mt1 = new MyTask(1, "任务1");
		MyTask mt2 = new MyTask(2, "任务2");
		MyTask mt3 = new MyTask(3, "任务3");
		MyTask mt4 = new MyTask(4, "任务4");
		MyTask mt5 = new MyTask(5, "任务5");
		MyTask mt6 = new MyTask(6, "任务6");

		pool.execute(mt1);
		pool.execute(mt2);
		pool.execute(mt3);
		pool.execute(mt4);
		pool.execute(mt5);
		// java.util.concurrent.RejectedExecutionException
		pool.execute(mt6);

		pool.shutdown();

	}
}

//==========================================================================================
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejected implements RejectedExecutionHandler {
    
    

	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
		System.out.println("自定义处理....");
		System.out.println("当前被拒绝任务为:" + r.toString());
	}
}

Concurrent.util常用类

CyclicBarrier使用

一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

假设有只有的一个场景:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待。示例

package cn.itcast_04;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class UseCyclicBarrier {
    
    

	static class Runner implements Runnable {
    
    
		private CyclicBarrier barrier;
		private String name;

		public Runner(CyclicBarrier barrier, String name) {
    
    
			this.barrier = barrier;
			this.name = name;
		}

		@Override
		public void run() {
    
    
			try {
    
    
				Thread.sleep(1000 * (new Random()).nextInt(5));
				System.out.println(name + " 准备OK.");
				barrier.await();
			} catch (InterruptedException e) {
    
    
				e.printStackTrace();
			} catch (BrokenBarrierException e) {
    
    
				e.printStackTrace();
			}
			System.out.println(name + " Go!!");
		}
	}

	public static void main(String[] args) throws IOException, InterruptedException {
    
    
		CyclicBarrier barrier = new CyclicBarrier(3); // 3
		ExecutorService executor = Executors.newFixedThreadPool(3);

		executor.submit(new Thread(new Runner(barrier, "zhangsan")));
		executor.submit(new Thread(new Runner(barrier, "lisi")));
		executor.submit(new Thread(new Runner(barrier, "wangwu")));

		executor.shutdown();
	}
}
CountDownLacth使用

它经常用于监听某些初始化操作,等初始化执行完毕后,通知主线程继续工作。

CountDownLatch概念

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务

CountDownLatch的用法

CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足

CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

个人理解:CountDownLatch:我把他理解成倒计时锁

场景还原 :一年级期末考试要开始了,监考老师发下去试卷,然后坐在讲台旁边玩着手机等待着学生答题,有的学生提前交了试卷,并约起打球了,等到最后一个学生交卷了,老师开始整理试卷,贴封条,下班,陪老婆孩子去了。

补充场景 :我们在玩LOL英雄联盟时会出现十个人不同加载状态,但是最后一个人由于各种原因始终加载不了100%,于是游戏系统自动等待所有玩家的状态都准备好,才展现游戏画面。

最后:由于CountDownLatch需要开发人员很明确需要等待的条件,否则很容易造成await()方法一直阻塞的情况。

示例

import java.util.concurrent.CountDownLatch;

/*
 * CountDownLatch类中最重要的方法
 * 		public void await() throws InterruptedException{};   
 * 			调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
 * 		public boolean await(long timeout, TimeUnit unit) throws InterruptedException{};  
 * 			和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
 * 		public void countDown(){};
 * 			将count值减1
 */
public class CountDownlatchTest {
    
    
	public static void main(String[] args) throws InterruptedException {
    
    
		CountDownLatch countDownLatch = new CountDownLatch(5);
		for (int i = 0; i < 5; i++) {
    
    
			new Thread(new readNum(i, countDownLatch)).start();
		}
		countDownLatch.await();
		System.out.println("线程执行结束。。。。");
	}

	static class readNum implements Runnable {
    
    
		private int id;
		private CountDownLatch latch;

		public readNum(int id, CountDownLatch latch) {
    
    
			this.id = id;
			this.latch = latch;
		}

		@Override
		public void run() {
    
    
			synchronized (this) {
    
    
				System.out.println("id:" + id);
				latch.countDown();
				System.out.println("线程组任务" + id + "结束,其他任务继续");
			}
		}
	}
}

示例二

package cn.itcast_04;

import java.util.concurrent.CountDownLatch;

public class UseCountDownLatch {
    
    

	public static void main(String[] args) {
    
    

		final CountDownLatch countDown = new CountDownLatch(2);

		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					System.out.println("进入线程t1" + "等待其他线程处理完成...");
					countDown.await();
					System.out.println("t1线程继续执行...");
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					System.out.println("t2线程进行初始化操作...");
					Thread.sleep(3000);
					System.out.println("t2线程初始化完毕,通知t1线程继续...");
					countDown.countDown();
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
		});
		Thread t3 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				try {
    
    
					System.out.println("t3线程进行初始化操作...");
					Thread.sleep(4000);
					System.out.println("t3线程初始化完毕,通知t1线程继续...");
					countDown.countDown();
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
			}
		});

		t1.start();
		t2.start();
		t3.start();
	}
}

注意:CyclicBarrier和CountDownLacth的区别

  • CountDownLacth是一个线程等待,其他的n个线程发出通知。针对的是一个线程
  • CyclicBarrier的线程都是参与阻塞的。针对的是多个线程。

在这里插入图片描述

Callable和Future使用(?)

这个例子其实就是我们之前实现的Future模式。jdk给予我们一个实现的封装,使用非常简单。

Future模式非常适合在处理很耗时很长的业务逻辑时进行使用,可以有效的减小系统的响应时间,提高系统的吞吐量。

在java多线程中,我们知道可以使用synchronized关键字来实现线程间的同步互斥工作,那么其实还有一个更优秀的机制去完成这个"同步互斥"工作,它就是Lock对象,我们主要学习两种锁,重入锁和读写锁。它们具有比synchronized为强大的功能,并且有嗅探锁定、多路分支等功能。

在JDK5.0版本之前,重入锁的性能远远好于synchronized关键字,JDK6.0版本之后synchronized 得到了大量的优化,二者性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。除此之外,重入锁又自带一系列高逼格UBFF:可中断响应、锁申请等待限时、公平锁。另外可以结合Condition来使用,使其更是逼格满满。

ReentrantLock(重入锁)

重入锁,在需要进行同步的代码部分加上锁定,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其他线程永远进不来的结果。

package cn.itcast_05;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UseReentrantLock {
    
    

	private Lock lock = new ReentrantLock();

	public void method1() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method1");
			Thread.sleep(1000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method1");
			Thread.sleep(1000);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void method2() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method2");
			Thread.sleep(1000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method2");
			Thread.sleep(1000);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public static void main(String[] args) {
    
    
		final UseReentrantLock ur = new UseReentrantLock();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				ur.method1();
				ur.method2();
			}
		}, "t1");

		t1.start();
		try {
    
    
			Thread.sleep(10);
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}
}

锁与等待/通知

还记得我们在使用synchronized的时候,如果需要多线程间进行协作工作则需要Object的wait()方法和notify()方法、notifyAll()方法进行配合工作。

那么同样,我们在使用Lock的时候,可以使用一个新的等待/通知的类,它就是Condition。这个Condition一定是针对具体某一把锁的。也就是在只有锁的基础之上才会产出Conditon。

package cn.itcast_05;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/*
 * 	打印结果:
 * 		当前线程:t1进入等待状态..
 * 		当前线程:t1释放锁..
 * 		当前线程:t2进入..
 * 		当前线程:t2发出唤醒..
 * 		当前线程:t1继续执行..
 */
public class UseCondition {
    
    

	private ReentrantLock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	public void method1() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入等待状态..");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "释放锁..");
			condition.await();// 等待 相当于Object类中的wait()方法
			System.out.println("当前线程:" + Thread.currentThread().getName() + "继续执行..");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void method2() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入..");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "发出唤醒..");
			condition.signal();// 唤醒 相当于Object类中的notify()方法
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public static void main(String[] args) {
    
    
		final UseCondition uc = new UseCondition();
		Thread t1 = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    
				uc.method1();
			}
		}, "t1");

		Thread t2 = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    
				uc.method2();
			}
		}, "t2");

		t1.start();
		t2.start();
	}
}

多Condition

我们可以通过一个Lock对象产生多个Condition进行多线程间的交互,非常的灵活。可以使得部分需要唤醒的线程唤醒,其他线程则继续等待通知。

package cn.itcast_05;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/*
 * 	打印结果
 * 		当前线程:t1进入方法m1等待..
 * 		当前线程:t2进入方法m2等待..
 * 		当前线程:t3进入方法m3等待..
 * 		当前线程:t4唤醒..
 * 		当前线程:t1方法m1继续..
 * 		当前线程:t2方法m2继续..
 * 		当前线程:t5唤醒..
 * 		当前线程:t3方法m3继续..
 */
public class UseManyCondition {
    
    

	private ReentrantLock lock = new ReentrantLock();
	private Condition c1 = lock.newCondition();
	private Condition c2 = lock.newCondition();

	public void m1() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m1等待..");
			c1.await();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m1继续..");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void m2() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m2等待..");
			c1.await();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m2继续..");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void m3() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入方法m3等待..");
			c2.await();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "方法m3继续..");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void m4() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "唤醒..");
			c1.signalAll();
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void m5() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "唤醒..");
			c2.signal();
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public static void main(String[] args) {
    
    

		final UseManyCondition umc = new UseManyCondition();
		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				umc.m1();
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				umc.m2();
			}
		}, "t2");
		Thread t3 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				umc.m3();
			}
		}, "t3");
		Thread t4 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				umc.m4();
			}
		}, "t4");
		Thread t5 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				umc.m5();
			}
		}, "t5");

		t1.start(); // c1
		t2.start(); // c1
		t3.start(); // c2

		try {
    
    
			Thread.sleep(2000);
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}

		t4.start();// c1 c2

		try {
    
    
			Thread.sleep(2000);
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}

		t5.start();// c2

	}
}

Lock/Condition其他方法和用法

公平锁和非公平锁(默认非公平锁)

Lock lock = new ReentrantLock(boolean isFair);

lock用法

  • tryLock():尝试获得锁,获得结果用true/false返回。默认是false
  • tryLock():在给定的时间内尝试获得锁,获得结果用true/false返回。
  • isFair():是否公平锁。如果此锁的公平设置为 true,则返回 true。
  • isLocked():是否锁定。查询此锁是否由任意线程保持。
  • getHoldCount():查询当前线程保持此锁的个数,也就是调用lock()次数。
  • lockInterruptibly():优先响应中断的锁。
  • getQueueLength():返回正等待获取此锁的线程估计数。
  • getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件Condition的线程估计数。
  • hasQueuedThread(Thread thread):查询给定线程是否正在等待获取此锁。
  • hasQueuedThreads():查询是否有些线程正在等待获取此锁。
  • hasWaiters(Condition condition):查询是否有些线程正在等待与此锁有关的condition条件。
package cn.itcast_05;
import java.util.concurrent.locks.ReentrantLock;
/*
 * lock.getHoldCount()方法:查询当前线程保持此锁的个数,也就是调用lock()次数。
 * 只能在当前调用的线程内部使用,不能再其他线程中使用
 * 那么我可以在m1方法里面去调用m2方法,同时m1方法和m2方法都持有lock锁定即可 测试结果holdCount数递增
 */
public class TestHoldCount {
    
    

	// 重入锁
	private ReentrantLock lock = new ReentrantLock();

	public void m1() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("进入m1方法,holdCount数为:" + lock.getHoldCount());
			// 调用m2方法
			m2();

		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public void m2() {
    
    
		try {
    
    
			lock.lock();
			System.out.println("进入m2方法,holdCount数为:" + lock.getHoldCount());
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			lock.unlock();
		}
	}

	public static void main(String[] args) {
    
    
		TestHoldCount thc = new TestHoldCount();
		thc.m1();
	}
}

ReentrantReadWriteLock(读写锁)

读写锁ReentrantReadWriteLock,其核心就是实现读写分离的锁。在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁。

之前学synchronized、ReentrantLock时,我们知道,同一时间内,只能有一个线程进行访问被锁定的代码,那么读写锁则不同,其本质是分成两个锁,即读锁、写锁。在读锁下,多个线程可以并发的进行访问,但是在写锁的时候,只能一个一个的顺序访问。

口诀 :读读共享,写写互斥,读写互斥。

package cn.itcast_06;

import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class UseReentrantReadWriteLock {
    
    

	// 创建读写锁
	private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
	private ReadLock readLock = rwLock.readLock();
	private WriteLock writeLock = rwLock.writeLock();

	public void read() {
    
    
		try {
    
    
			readLock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			readLock.unlock();
		}
	}

	public void write() {
    
    
		try {
    
    
			writeLock.lock();
			System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
			Thread.sleep(3000);
			System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			writeLock.unlock();
		}
	}

	public static void main(String[] args) {
    
    
		final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();

		Thread t1 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				urrw.read();
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				urrw.read();
			}
		}, "t2");
		Thread t3 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				urrw.write();
			}
		}, "t3");
		Thread t4 = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				urrw.write();
			}
		}, "t4");

		// t1.start();
		// t2.start();

		// t1.start(); // R
		// t3.start(); // W

		t3.start();
		t4.start();
	}
}

锁优化总结

1、避免死锁

2、减小锁的持有时间

  • 例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁;

3、减小锁的粒度

  • 例如:ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;

4、锁的分离

  • 根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性。最常见的锁分离就是读写锁ReadWriteLock。

5、锁粗化

  • 这看起来与思路1有冲突,其实不然。思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间;
  • 而锁粗化是指:在一个间隔性地需要执行同步语句的线程中,如果在不连续的同步块间频繁加锁解锁是很耗性能的,因此把加锁范围扩大,把这些不连续的同步语句进行一次性加锁解锁。虽然线程持有锁的时间增加了,但是总体来说是优化了的。

6、锁消除

  • 锁消除是编译器做的事:根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程(即不会影响线程空间外的数据),那么可以认为这段代码是线程安全的,不必要加锁。

7、尽量使用无锁操作,如原子操作(Atomic系列类),volatile关键字。


猜你喜欢

转载自blog.csdn.net/weixin_44950987/article/details/108302373
今日推荐