计算机历史
At the all of begining
这么大一台计算机只能从头到尾执行只能一个程序,浪费资源哇。
后来操作系统出现了
--计算机终于能运行多个程序啦,一个程序就是一个单独的进程。一个进程中又有一个或多个线程,cpu来回在不同的线程中切换执行,从此cpu资源不再浪费。
串行与并行
串行与并行,什么是并发编程?以及并发编程有什么好处?
线程不安全操作代码实例
package com.xdclass.synopsis;
import java.util.concurrent.CountDownLatch;
/**
* 线程不安全操作代码实例
*/
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次调用对num进行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch,计数减1
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
}
运行,num=937,显然和预期的结果不符。
剖析线程不安全的原因
javac -encoding UTF-8 UnsafeThread.java 编译成.class
javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令
0: getstatic #2 获取指定类的静态域,并将其押入栈顶
3: iconst_1 将int型1押入栈顶
4: iadd 将栈顶两个int型相加,将结果押入栈顶
5: putstatic #2 为指定类静态域赋值
8: return
例子中,产生线程不安全问题的原因:
num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程不安全。
synchronized
内置锁
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。
互斥锁
内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
synchronized 可以修饰那些方法?
- 修饰普通方法:锁住的是对象的实例,当两个线程去访问两个不同的实例,几乎同时输出无需等待,说明访问另一个实例并没有影响,因此修饰普通方法只是锁住当前这个实例。
- 修饰静态方法:锁住整个类,当两个线程去访问两个不同的实例,一个线程输出之后要等待,才轮到下一个线程输出,说明修饰静态方法是锁住当前这个类的,这种方法比较危险,实际开发中不建议使用。
- 修饰代码块: 锁住一个对象 synchronized (lock),即synchronized后面括号里的内容,只有获得括号里的内容才执行。
/**
* 深入理解synchronized关键字
*/
public class SynDemo {
//修饰普通方法
public synchronized void out() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
//修饰静态方法
public static synchronized void staticOut() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000L);
}
//修饰代码块
private Object lock = new Object();
public void myOut() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynDemo synDemo = new SynDemo();
new Thread(() -> {
synDemo.myOut();
}).start();
new Thread(() -> {
synDemo.myOut();
}).start();
}
}
单例与线程安全
1.单例常用写法有哪些?
-
饿汉式----- 在类加载的时候,在类的内部使用
static
关键字对类本身进行实例化 -
懒汉式----- 访问
getInstance()
方法时,判断如果没有这个类的实例,new一个
2.这两种单例写法是线程安全的吗?即多个线程访问,返回的是不是同一个实例?
package com.xdclass.safe;
/**
* 饿汉式单例
* 在类加载的时候,就已经进行实例化,无论之后用不用到。
* 如果该类比较占内存,之后又没用到,就白白浪费了资源。
*/
public class HungerSingleton {
private static HungerSingleton ourInstance = new HungerSingleton();
public static HungerSingleton getInstance() {
return ourInstance;
}
private HungerSingleton() {
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(HungerSingleton.getInstance());
}).start();
}
}
}
运行这段代码,我们发现十个线程并发访问,输出的是同一个实例,因此饿汉式的写法是线程安全的。
懒汉式单例的简单写法
/**
* 懒汉式单例
* 在需要的时候再实例化
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//判断实例是否为空,为空则实例化
if (null == lazySingleton) {
//模拟实例化时耗时的操作
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton = new LazySingleton();
}
//否则直接返回
return lazySingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
运行这段代码,我们发现十个线程并发访问,输出的不是同一个实例,因此懒汉式的简单写法
是线程非安全的。
如何改进呢?
package com.xdclass.safe;
/**
* 懒汉式单例
* 在需要的时候再实例化
*/
public class LazySingleton {
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//判断实例是否为空,为空则实例化
if (null == lazySingleton) {
//模拟实例化时耗时的操作
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LazySingleton.class) {
if (null == lazySingleton) {
lazySingleton = new LazySingleton();
}
}
}
//否则直接返回
return lazySingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
这段代码使用了volatile
关键字防止了指令重排序。
另外使用synchronized
关键字保证了实例化操作是原子性的,另外还要再加一层判断,防止有多个线程已经进入了前一层判断而new出多个实例,导致线程的不安全。因此这种写法才是线程安全的。
不直接在getInstance()
方法加锁是因为这样效率低下。
线程池
为什么使用线程池?
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如何创建一个线程池?
Java中已经提供了创建线程池的一个类:Executor,而我们创建时,一般使用它的子类:ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
这是这个类中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
线程池的执行流程又是怎样的呢?
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
执行结果可以看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。如果上面程序中,将for循环中改成执行20个任务,就会抛出任务拒绝异常了。因为最大线程数又不够,队列又不够。
参考:Java并发编程:线程池的使用
handler的拒绝策略:
有四种: 第一种AbortPolicy:该策略直接抛出异常,阻止系统正常工作
第二种DisCardPolicy:直接啥事都不干,直接把任务丢弃
第三种DisCardOldSetPolicy:丢弃最老的一个请求(任务队列里面的第一个),再尝试提交任务
第四种CallerRunsPolicy: 直接调用execute来执行当前任务
四种常见的线程池:
CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
FixedThreadPool:固定数目的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
参考:深入理解线程和线程池
线程池的使用建议
尽量避免使用Executor框架创建线程池,原因:
newFixedThreadPool newSingleThreadExecutor
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
newCachedThreadPool newScheduledThreadPool
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
为什么第二个例子,在限定了堆的内存之后,还会把整个电脑的内存撑爆
创建线程时用的内存并不是我们制定jvm堆内存,而是系统的剩余内存。(电脑内存-系统其它程序占用的内存-已预留的jvm内存)
创建线程池时,核心线程数不要过大
相应的逻辑,发生异常时要处理
submit 如果发生异常,不会立即抛出,而是在get的时候,再抛出异常
execute 直接抛出异常
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。