前言:
本篇着重于实战应用,对于其原理并没有过多阐述,其目的是为了达到前期学习的快速开发。
写出第一个线程并开启它
方式一:
- 想要创建一个线程,首先得继承一个Thread类。
- 重写run()方法,具体的线程要执行的内容需要写在run()方法里。
- new线程对象(可以new多个),开启线程(也可同时开启多个)
public class ThreadTest extends Thread {
private String name;
public ThreadTest(String name) {
this.name = name;
}
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(name+"下载了"+i+"%");
}
}
public static void main(String[] args) {
ThreadTest thread_01 = new ThreadTest("线程1");
thread_01.start();
ThreadTest thread_02 = new ThreadTest("线程2");
thread_02.start();
}
}
方式二:实现Runnable接口来创建线程
public class ThreadTest implements Runnable {
private String name;
public ThreadTest(String name) {
this.name = name;
}
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(name+"下载了"+i+"%");
}
}
public static void main(String[] args) {
Thread thread_01 = new Thread(new ThreadTest("线程1"));
thread_01.start();
Thread thread_02 = new Thread(new ThreadTest("线程2"));
thread_02.start();
}
}
线程一般用在哪里?如何去用?
要知道,程序执行本身就是一个线程(单线程)。那么既然用了线程,自然就是多线程。
在需要增大吞吐量的时候
在做WEB时,容器帮你做了多线程,但是他只能帮你做请求层面的。简单的说,就是一个请求一个线程。或多个请求一个线程。如果是单线程,那同时只能处理一个用户的请求。
多核时代离不开多线程
我们可以通过增加CPU核数(现在电脑普遍8核)来提升性能。如果是单线程,那程序执行到死也就利用了单核,没办法通过增加CPU核数来提升性能。
举个例子
假如有一个请求,请求服务器执行三个很慢的CRUD操作。正常顺序为:
- 第一次读取 (9ms)
- 处理第一次的数据(1ms)
- 第二次读取 (9ms)
- 处理第二次的数据(1ms)
- 第三次读取 (9ms)
- 处理第三次的数据(1ms)
- 对三次处理结果进行整合返回 (1ms)
如此,单线程耗时31ms。
但是,如果把三个操作分给3个线程去做,就只需要11ms。此执行方案和木桶原理一样,耗时取决于最慢的那个线程的执行速度。
线程池
还用上面的例子。我们会发现一个问题。如果其中一个操作占用时间特别长,多线程似乎并没有节约太多时间。就比如,我现在将上面的时间稍作体调整。
假如有一个请求,请求服务器执行三个CRUD操作。正常顺序为:
- 第一次读取 (1ms)
- 处理第一次的数据(1ms)
- 第二次读取 (1ms)
- 处理第二次的数据(1ms)
- 第三次读取 (20ms)
- 处理第三次的数据(1ms)
- 对三次处理结果进行整合返回 (1ms)
如此,单线程耗时26ms。
但对于这种情况,即便是多线程,取最长的那个线程时间,最后还是需要22ms,仅仅节约了4ms,万一中途线程调度切换再浪费几ms。这似乎就得不偿失了。
试想有100个用户同时搞这样一个操作,那耗时就是100*22ms了。那如果我们把第一次那22ms的操作中从数据库里读取的数据进行缓存,这样后面的用户就不需要再次读取了。
为解决上面的问题,在此就引入线程池的概念了。
线程池就是首先创建好一个大小一定的池子,每次提交一个任务就会创建一个线程,直至线程数量达到线程池容纳上限。用完后线程对象不销毁,存储在这个“池子”里面,之后用几个取几个,用完后还放回到“池子”里面去。如果在所有线程都处于活动状态时,这时再有其他任务提交,他们将在等待队列中等候直到有空闲的线程可用。如果任何线程由于执行过程中的故障而终止,将会有一个新线程将取代这个线程执行后续任务。
如何创建一个线程池?
创建可以容纳3个线程的线程池(固定大小)
ExecutorService threadPool = Executors.newFixedThreadPool(3);
创建一个动态大小的线程池(会根据需要自动创建线程)
ExecutorService threadPool = Executors.newCachedThreadPool(); 线程池的大小会根据执行的任务数量动态分配
详细内容参考博文:java线程池(此处引用的是他人博客)
注意:
切忌new Thread(…).start() 的滥用。对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:
- 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
- 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的
- 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题
简而言之:你知道多线程会有多多吗?
线程休眠
就是加了个sleep()方法。在线程对象后面 . 了一个sleep(休眠多少毫秒)
线程锁
简析Synchronized 同步静态方法和非静态方法(涉及到并发,暂时用的不多,简单了解。)
Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
Java中每个对象都有一个锁,并且是唯一的。假设分配的一个对象空间,里面有多个方法,相当于空间里面有多个小房间,如果我们把所有的小房间都加锁,因为这个对象只有一把钥匙,因此同一时间对象只能打开一个小房间,然后用完了还回去。
情况1: 同一个对象在两个线程中分别访问该对象的两个同步方法
结果:会产生互斥。
解释:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。
情况2:不同对象在两个线程中调用同一个同步方法
结果:不会产生互斥。
解释:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”
情况1: 用类直接在两个线程中调用两个不同的同步方法
结果:会产生互斥。
解释:因为对静态对象加锁实际上是对类(.class)加锁,类只有一个,可以理解为任何时候都只有一个空间(对象虽然可以有多个,但类只有一个),里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。
情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法
结果:会产生互斥。
解释:因为是一个对象调用,同上。
情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法
结果:不会产生互斥
解释:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。
详情请参考博文:线程锁(此处引用的连接是他人的博客)