一、什么是线程安全问题

为什么有线程安全问题?

当多个线程同时共享同一个全局变量或静态变量,做写的操作(修改变量值)时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作时不会发生数据冲突问题。

案例:需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。

/*** 需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。* Created by yz on 2018/04/01.*/
public class ThreadDemo {public static void main(String[] args) {// t1 t2同时共享同一变量trainCountThreadTrain threadTrain = new ThreadTrain();Thread t1 = new Thread(threadTrain, "窗口1");Thread t2 = new Thread(threadTrain, "窗口2");t1.start();t2.start();}
}// 售票窗口
class ThreadTrain implements Runnable{// 总共有100张火车票private int trainCount = 100;public void run() {while (trainCount > 0){try {// 休眠50秒Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 出售火车票sale();}}// 卖票方法public void sale(){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}
}

运行结果:


原因解析:
卖票方法加判断,不能百分百解决问题

// 卖票方法
public void sale(){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}
}

原因解析:


多个线程共享同一个局部变量,会发生线程安全问题吗?不会


线程安全问题,有哪些解决办法?

解决办法:
synchronized — 自动锁
lock — jdk1.5并发包 — 手动锁

线程之间如何同步?
同步是保证数据原子性,原子性就是数据不能受到其他线程干扰。

二、使用同步代码块解决线程安全问题

什么地方需要考虑加锁?
考虑在真正产生共享同一个全局变量的时候使用,不要用synchronized去包裹整个代码。

// 卖票方法
public void sale(){// 同步代码块 synchronized 包裹需要线程安全的问题。synchronized (object){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}}
}

什么是同步代码块?
答:就是将可能会发生线程安全问题的代码,给包裹起来
synchronized(对象){ // 这个对象可以为任意对象
可能会发生线程冲突的问题
}

对象如同锁,持有锁的线程可以在同步块中执行,没持有锁的线程,即使获取cup的执行权,也进不去。

使用synchronized必须有一些条件:
1.必须要有两个或者两个以上的线程需要发生同步。
2.多个线程想同步,必须使用同一把锁

3.保证只有一个线程进行执行

synchronized原理:
1.首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。
2.锁是在什么时候释放?代码执行完毕或者程序抛出异常都会被释放掉。
3.锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。

好处:解决了多线程的安全问题
弊端:效率非常低,多个线程需要判断锁,比较消耗资源,抢锁的资源。

什么是线程之间同步?
同步是保证多个线程之间共享同一个全局变量数据安全问题,保证数据原子性。

synchronized 只能有一个线程进行执行(就像厕所只有一个坑,好多人等着上厕所,谁进入厕所谁上锁,其他人在外等待),这个线程不释放锁的话,其他线程就一直等,就会产生死锁问题。

三、同步函数使用、this锁

同步函数
什么是同步函数?
答:在方法上加上synchronized进行修饰 称为同步函数。

// 卖票方法
public synchronized void sale(){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}
}

同步函数使用的是什么锁?this锁
怎么证明同步函数使用的是this锁?
两个线程之间实现同步,一个线程使用this锁同步代码块,一个线程使用同步函数,这两个线程如果同步,说明同步函数使用的是this锁。

/*** 需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。* Created by yz on 2018/04/01.*/
public class ThreadDemo {public static void main(String[] args) throws InterruptedException {// t1 t2同时共享同一变量trainCountThreadTrain threadTrain = new ThreadTrain();Thread t1 = new Thread(threadTrain, "窗口1");Thread t2 = new Thread(threadTrain, "窗口2");t1.start();Thread.sleep(40);threadTrain.flg = false;t2.start();}
}// 售票窗口
class ThreadTrain implements Runnable{// 总共有100张火车票private int trainCount = 100;public boolean flg = true;public void run() {if(flg){while (trainCount > 0) {try {// 休眠50秒Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 执行同步代码块 this锁sale1();}}else {// 执行同步函数while (trainCount > 0){try {// 休眠50秒Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}// 执行同步函数sale();}}}// 卖票方法 同步函数public synchronized void sale(){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}}// 卖票方法public void sale1(){// 同步代码块 synchronized 包裹需要线程安全的问题。this锁synchronized (this){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}}}
}

面试问题:

一个线程使用同步函数,另一个线程使用同步代码块this锁,能够同步吗?可以同步。

一个线程使用同步函数,另一个线程使用同步代码块(非this锁),能够同步吗?不能同步。
this锁 synchronized(参数)只要是任何一个类型都可以把它叫做锁

public void sale(){// 同步代码块 synchronized 包裹需要线程安全的问题。synchronized (this){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}}
}

四、静态同步代码块

同步函数分为:

1.非静态同步函数:
public synchronized void sale(){}

2.静态(static关键字)同步函数,还会用this锁吗?
public static synchronized void sale(){}
静态同步函数不使用this锁,不同步。
当一个变量被static修饰的话存放在内存永久区,当class文件被加载的时候会被初始化。
private static int trainCount = 100;
静态同步函数与当前字节码文件一起使用可以同步。

// 卖票方法 同步函数
public static synchronized void sale(){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}
}// 卖票方法
public void sale1(){// 同步代码块 synchronized 包裹需要线程安全的问题。this锁synchronized (ThreadTrain.class){if(trainCount > 0){System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");trainCount--;}}
}

两个线程,一个线程使用同步函数,另一个线程使用静态同步函数能实现同步吗?

不能,同步函数使用this锁,静态同步函数使用当前字节码文件(字节码文件就是当前class文件)。

加锁保证同步,同步保证数据安全问题、原子问题。

使用synchronized、lock都属于单个jvm中同步。分布式锁、高并发和jvm同步是没有任何关系,是和集群有关系。

五、多线程死锁

什么是多线程死锁?
答:同步中嵌套同步,导致锁无法释放

六、Java内存模型

多线程三大特性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。
可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码先后顺序执行。

java内存模型和java内存结构区别?

java内存模型属于多线程可见性 jmm

java内存结构是jvm内存分配

七、Volatile可见性

什么是Volatile

Volatile 关键字的作用是变量在多个线程之间可见,但不保证原子性。

线程可见性:线程1本地内存的变量值发生改变之后,立马通知给另一个线程。

/*** Volatile可见性* Created by yz on 2018/4/2.*/
public class ThreadVolatile {public static void main(String[] args) throws InterruptedException {ThreadVolatileDemo td = new ThreadVolatileDemo();td.start();Thread.sleep(3000);// 主线程修改了共享全局变量为falsetd.setFlg(false);System.out.println("flg值已经修改为false!");Thread.sleep(1000);System.out.println(td.flg);}
}class ThreadVolatileDemo extends Thread{public boolean flg = true;@Overridepublic void run() {System.out.println("子线程开始执行...");while(flg){// ThreadVolatileDemo线程依然读取的是本地线程内存值 }System.out.println("子线程执行结束...");}public void setFlg(boolean flg){this.flg = flg;}
}

问题,当flg值修改为false后,程序一直停止不了


解决,使用volatile关键字强制刷新主内存

public volatile boolean flg = true;
如果没有添加休眠时间,在不添加volatile关键字时,修改flg值后,flg值是实时修改,程序执行完毕结束。
主线程添加休眠时间后,在不添加volatile关键字时,修改flg值后,不会及时通知给子线程。

八、AtomicInteger原子类

创建10个线程,每个线程执行1000次,共享count变量。

/*** Volatile非原子性* Created by yz on 2018/4/2.*/
public class VolatileNoAtomic extends Thread{// 需要10个线程同时共享count  static修饰关键字,存放在静态区,只会存放一次,所有线程都会共享private volatile static int count = 0;@Overridepublic void run() {for (int i = 0; i < 1000; i++) {count++;}System.out.println(getName()+","+count);}public static void main(String[] args) {// 创建10个线程VolatileNoAtomic[] vaList = new VolatileNoAtomic[10];for (int i = 0; i < vaList.length; i++) {vaList[i] = new VolatileNoAtomic();}for (int i = 0; i < vaList.length; i++) {vaList[i].start();}}
}

加上volatile关键字数据也不准确

java并发包:java.util.concurrent jdk1.5


AtomicInteger原子类,用于计数

/*** AtomicInteger原子类* Created by yz on 2018/4/2.*/
public class VolatileNoAtomic extends Thread{// 需要10个线程同时共享count  static修饰关键字,存放在静态区,只会存放一次,所有线程都会共享//private volatile static int count = 0;private static AtomicInteger count = new AtomicInteger(0);@Overridepublic void run() {for (int i = 0; i < 1000; i++) {//count++;// 以原子方式将当前值加 1count.incrementAndGet();}System.out.println(getName()+","+count.get());}public static void main(String[] args) {// 创建10个线程VolatileNoAtomic[] vaList = new VolatileNoAtomic[10];for (int i = 0; i < vaList.length; i++) {vaList[i] = new VolatileNoAtomic();}for (int i = 0; i < vaList.length; i++) {vaList[i].start();}}
}

九、ThreadLock原理剖析

import java.util.Map;/*** Created by yz on 2018/04/02.*/
public class ThreadLocal01 {public static void main(String[] args) {// 怎样不共享ResNumber,每个线程都用自己的ResNumber,不使用new
//        ResNumber resNumber1 = new ResNumber();
//        ResNumber resNumber2 = new ResNumber();
//        ResNumber resNumber3 = new ResNumber();ResNumber resNumber = new ResNumber();LocalThreadDemo t1 = new LocalThreadDemo(resNumber);LocalThreadDemo t2 = new LocalThreadDemo(resNumber);LocalThreadDemo t3 = new LocalThreadDemo(resNumber);t1.start();t2.start();t3.start();}
}/*** 使用ThreadLocal , ResNumber不共享,在每个线程中使用,互不影响*/
class ResNumber{public int count = 0;public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){protected Integer initialValue(){return 0;}};public String getNumber(){count = threadLocal.get()+1;threadLocal.set(count);return count+"";}// ThreadLocal get set伪代码Map<Object,Object> map;public void set(Integer count){// Thread.currentThread() 获取当前线程的引用map.put(Thread.currentThread(),count);}public String get(){return (String) map.get(Thread.currentThread());}
}class LocalThreadDemo extends Thread{private ResNumber resNumber;public LocalThreadDemo(ResNumber resNumber){this.resNumber = resNumber;}@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(getName()+","+resNumber.getNumber());}}
}

多线程并发同步解决方案

业务场景1:

业务需求1:假如现在有20个人去售票厅窗口买票,但是窗口只有2个,那么同时能够买票的只能有2个人,当2个人中任意1个人买完票离开窗口之后,等待的18个人中又会有一个人可以占用窗口买票。

真实需求:控制并发数为2
拆解转化业务需求:
人=线程
2个窗口=资源
在窗口买票=表示线程正在执行
离开售票窗口=线程执行完毕
等待买票=线程阻塞,不能执行

解决方案:信号量 Semaphore
应用场景:用于流量控制,限流

代码:

/*** 信号量 Semaphore* Created by yz on 2018/3/4.*/
public class SemaphoreDemo {undefinedclass MyTask implements Runnable{undefinedprivate Semaphore semaphore; // 信号量private int user;  // 第几个用户public MyTask(Semaphore semaphore, int user) {undefinedthis.semaphore = semaphore;this.user = user;}@Overridepublic void run() {undefinedtry {undefined// 获取信号量许可,才能占用窗口semaphore.acquire();// 运行到这里说明获取到了许可,可以去买票了System.out.println("用户"+ user + "进入窗口,准备买票...");Thread.sleep((long)Math.random()*10000); // 模拟买票时间System.out.println("用户"+ user + "买票完成,准备离开...");Thread.sleep((long)Math.random()*10000);System.out.println("用户"+ user + "离开售票窗口...");// 释放信号量许可证semaphore.release();} catch (InterruptedException e) {undefinede.printStackTrace();}}}private void execute(){undefined// 定义窗口个数final Semaphore s = new Semaphore(2);// 线程池ExecutorService threadPool = Executors.newCachedThreadPool();// 模拟20个用户for (int i = 0; i < 20; i++) {undefinedthreadPool.execute(new MyTask(s,(i+1)));}// 关闭线程池threadPool.shutdown();}public static void main(String[] args) {undefinedSemaphoreDemo semaphoreDemo = new SemaphoreDemo();semaphoreDemo.execute();}}

业务场景2:

业务需求2:公司周末组织去聚餐,首先各自从家里出发到聚餐地点,当所有人全部到齐之后,才开始吃饭 如果人员未到齐,到的人就只能等待在那里,直到所有人都到齐之后才能吃饭或者做后面的事情。

解决方案:同步屏障 CyclicBarrier

应用场景:用于多线程计算数据,最后合并计算结果,例如多个老师打分,最后合并算平均分

代码:

/*** 同步屏障 CyclicBarrier* Created by yz on 2018/3/4.*/
public class CyclicBarrierDemo {undefinedpublic static void main(String[] args) {undefinedfinal CyclicBarrier cb = new CyclicBarrier(3, new Runnable() {undefined@Overridepublic void run() {undefined// 在吃饭之前做点别的事情System.out.println("人员全部到齐了,拍照留念...");try {undefinedThread.sleep(3000);} catch (InterruptedException e) {undefinede.printStackTrace();}}});// 线程池ExecutorService threadPool = Executors.newCachedThreadPool();// 模拟3个用户for (int i = 0; i < 3; i++) {undefinedfinal int user = i+1;Runnable r = new Runnable() {undefined@Overridepublic void run() {undefinedtry {undefined// 模拟每个人来的时间各不一样Thread.sleep((long)Math.random()*10000);System.out.println(user+"到达聚餐地点,当前已有"+ (cb.getNumberWaiting()+1) +"人到达");// 设置屏障 等待,只有当线程都到达之后,才能往下面走cb.await();if(user == 3){undefinedSystem.out.println("人员全部到齐,开始吃饭...");}Thread.sleep((long)Math.random()*20000);System.out.println(user+"吃完饭了,准备回家...");// CyclicBarrier 可以重复使用 doSomething} catch (Exception e) {undefinede.printStackTrace();}}};threadPool.execute(r);}threadPool.shutdown();}}

业务场景3:

业务需求3:假如A团伙绑了B,告诉B的家人C,需要1000万赎人,A与C达成一致意见到某一个地点交换人质,于是,A团伙和C同时到达交换地点,然后同时一手交钱一手交人质。
解决方案:Exchanger 两个线程之间进行数据交换

应用场景:用于两个线程之间交换数据,例如校对工作

代码:

/*** Exchanger 两个线程交换数据* Created by yz on 2018/3/4.*/
public class ExchangerDemo {undefinedpublic static void main(String[] args) {undefined// 定义交换器,交换String 类型的数据,当然是可以为任意类型Exchanger<String> exchanger = new Exchanger<>();// 定义线程池ExecutorService threadPool = Executors.newCachedThreadPool();// 绑架者AthreadPool.execute(new Runnable() {undefined@Overridepublic void run() {undefinedtry {undefined// 准备人质String renzhi = "B";String money = exchanger.exchange(renzhi);System.out.println("绑架者用B交换回:"+money);} catch (InterruptedException e) {undefinede.printStackTrace();}}});// 家属CthreadPool.execute(new Runnable() {undefined@Overridepublic void run() {undefinedtry {undefined// 准备1000万String money = "1000万";String renzhi = exchanger.exchange(money);System.out.println("C用1000万交换回:"+renzhi);} catch (InterruptedException e) {undefinede.printStackTrace();}}});threadPool.shutdown();}
}

业务场景4:

业务需求4:比如有一个任务A,他需要等待其他几个任务(BCD)都执行完毕之后才能执行这个任务
解决方案:ConutDownLatch 倒计时器
应用场景:可以用于模拟高并发

ConutDownLatch 与 CyclicBarrier 区别:
共同点:都能够实现线程之间的等待
不同点:
ConutDownLatch 一般用于某个线程A等待若干个其他线程执行完任务之后,它才能执行
CyclicBarrier 一般用于一组线程互相等待到某个状态,然后这一组线程在同时执行
ConutDownLatch 是不能重用的,CyclicBarrier 可以重复使用

代码:

/*** ConutDownLatch 倒计时器* Created by yz on 2018/3/4.*/
public class ConutDownLatchDemo {undefinedpublic static void main(String[] args) {undefined// 定义倒计时器final CountDownLatch latch = new CountDownLatch(3);// 模拟一个子任务Bnew Thread(){undefined@Overridepublic void run() {undefinedtry {undefined// 模拟任务执行时间Thread.sleep((long)Math.random()*10000);System.out.println("子任务B"+Thread.currentThread().getName()+"正在执行");Thread.sleep((long)Math.random()*10000);System.out.println("子任务B"+Thread.currentThread().getName()+"执行完毕");// 倒计时减掉1latch.countDown();} catch (InterruptedException e) {undefinede.printStackTrace();}}}.start();// 模拟一个子任务Cnew Thread(){undefined@Overridepublic void run() {undefinedtry {undefined// 模拟任务执行时间Thread.sleep((long)Math.random()*10000);System.out.println("子任务C"+Thread.currentThread().getName()+"正在执行");Thread.sleep((long)Math.random()*10000);System.out.println("子任务C"+Thread.currentThread().getName()+"执行完毕");// 倒计时减掉1latch.countDown();} catch (InterruptedException e) {undefinede.printStackTrace();}}}.start();// 模拟一个子任务Dnew Thread(){undefined@Overridepublic void run() {undefinedtry {undefined// 模拟任务执行时间Thread.sleep((long)Math.random()*10000);System.out.println("子任务D"+Thread.currentThread().getName()+"正在执行");Thread.sleep((long)Math.random()*10000);System.out.println("子任务D"+Thread.currentThread().getName()+"执行完毕");// 倒计时减掉1latch.countDown();} catch (InterruptedException e) {undefinede.printStackTrace();}}}.start();// main 线程为主任务ASystem.out.println("等待3个子任务执行完毕"+Thread.currentThread().getName()+"主任务才开始执行");try {undefined// 等待子任务执行完毕 此时阻塞latch.await();System.out.println("说明BCD三个子任务已经执行完毕");// 继续执行主任务System.out.println("继续执行主任务:"+Thread.currentThread().getName());} catch (InterruptedException e) {undefinede.printStackTrace();}}}

(四)Java中的多线程之间实现同步+多线程并发同步相关推荐

  1. Java中的硬件事务性内存,或者为什么同步将再次变得很棒

    总览 硬件事务内存有潜力允许多个线程同时以推测方式访问相同的数据结构,并使缓存一致性协议确定是否发生冲突. HTM旨在为您提供细粒度锁定的可伸缩性,粗粒度锁定的简单性以及几乎没有锁定的性能. 如果JV ...

  2. java中什么是线程安全_Java 多线程:什么是线程安全性

    线程安全性 什么是线程安全性 <Java Concurrency In Practice>一书的作者 Brian Goetz 是这样描述"线程安全"的:"当多 ...

  3. Java中Integer包装类之间的比较问题

    Integer 是Java中的包装类,通常情况下,两个相同值的Integer对象实例用 "==" 运算符进行比较时,返回结果应该是false,但是实际情况却不是这样. public ...

  4. cucumber java_如何在Cucumber Java中的步骤之间传递变量值?

    在Cucumber for Java(cucumber-jvm)中,在步骤之间共享数据的预期方式是使用依赖性集成(DI)容器 – 其中一些已与Cucumber集成. 使用DI的方法在容器之间略有不同, ...

  5. cas客户端登陆状态不同步_Java并发——同步组件

    特指Java.util.concurrent(JUC)包下的同步组件,包括AbstractQuenedSynchronizer(AQS).ReentrantLock.CyclicBarrier等. 关 ...

  6. java中线程调度遵循的原则_Java 多线程(三) 线程的生命周期及优先级

    线程的生命周期 线程的生命周期:一个线程从创建到消亡的过程. 如下图,表示线程生命周期中的各个状态: 线程的生命周期可以分为四个状态: 1.创建状态: 当用new操作符创建一个新的线程对象时,该线程处 ...

  7. java datetime int_关于jodatime:Java中DateTime对象之间的小数天数

    如何查找两个Joda Time日期时间实例之间的天数分数差异? days.daysBetween(start,end).getdays()给我一个类似10/15等的整数.我需要得到的是精确的值,如10 ...

  8. Guava关于JAVA中系统组件之间交互通讯(非线程之间通讯)

    2019独角兽企业重金招聘Python工程师标准>>> Guava EventBus组件 // Class is typically registered by the contai ...

  9. java中怎么通过日期算出天数_讲解对于Java中如何计算日期之间的天数知识

    在Java之中用Calendar方法,我们可以容易的实现日期相关的计算: public class TestDate { public static void main(String[] args) ...

  10. java中判断 101-200 之间有多少个素数,并输出所有的素数

    题目:判断 101-200 之间有多少个素数,并输出所有的素数 素数是什么: 质数又称素数.一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数:否则称为合数. 那么题目的答案如下: ...

最新文章

  1. python3字典详解_python3中字典详解
  2. T-PAMI 2021 | 换个损失函数就能实现数据扩增?
  3. mongodb 关闭服务 mongod -f /root/mongodb/bin/xx.conf --shutdown
  4. CMM与CMMI的关系
  5. not found on server registry
  6. automapper自动创建映射_ASP.NET Core教程:ASP.NET Core使用AutoMapper
  7. 零基础学前端可行吗?要如何学习呢?
  8. oracle sha2,Oracle11.2.0.1在AMD CPU 64位硬件,32位操作系统下的BUG 8670579
  9. python及拓展版_python扩展模块
  10. TensorFlow:交叉熵损失函数
  11. 适应adblock plus 规则的简单正则表达式匹配
  12. 装微软原版win10系统
  13. 教师计算机技术培训内容,2019教师计算机培训计划
  14. react native 随手记之打包遇到坑
  15. 艺术留学|工业设计专业2019大学新排名
  16. html中竖线怎么写,HTML如何写出竖线
  17. 关于Android方向传感器的终极解释
  18. 数据结构之顺序表(Java实现)
  19. 运放_电流互感器电流检测电路
  20. 丁腈橡胶自然老化时间_影响丁腈橡胶老化的因素研究

热门文章

  1. 方维模板修改,发布分享、主题有商品时,标签需自动写到input里,不要再手动去点击添加,手动点击可取消...
  2. 记一次mysql千万订单汇总查询优化
  3. oralce EM企业管理器
  4. 我的Spring MVC第一个应用
  5. 仿百度手机助手标题栏透明度随ListView或ScrollView滚动改变的实现方法
  6. 【转】Asp.Net MVC详解Controller之Filter
  7. 解决log4j:WARN Error initializing output writer. log4j:WARN Unsupported encoding?的问题
  8. java抛出异常thorw和throws的用法
  9. 《统计学习方法》代码全解析——第三部分k近邻法
  10. 程序员如何年薪百万?深度学习必读书籍!