计算机历史

At the all of begining
clipboard.png
这么大一台计算机只能从头到尾执行只能一个程序,浪费资源哇。

后来操作系统出现了
clipboard.png
--计算机终于能运行多个程序啦,一个程序就是一个单独的进程。一个进程中又有一个或多个线程,cpu来回在不同的线程中切换执行,从此cpu资源不再浪费。

串行与并行

串行与并行,什么是并发编程?以及并发编程有什么好处?
clipboard.png

线程不安全操作代码实例

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,显然和预期的结果不符。

clipboard.png

剖析线程不安全的原因

 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)

这是这个类中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:

clipboard.png

线程池的执行流程又是怎样的呢?

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 直接抛出异常

Nirvana
32 声望5 粉丝

整个体系复杂对我而言又陌生,每次学习对自己的脑力与知识体系都是一个巨大的挑战,也需要克服巨大的惰性;巨大的挑战同时也意味着巨大的诱惑。意味着我搞懂了,就能超越平凡的大多数,能力又上升了一个台阶。一...