java多线程操作
概述
在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。
Java是最先支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,因此Java开发者能常遇到上面描述的问题场景
一、相关概念
-
程序与进程
程序是一组有序指令的集合,是一种静态的概念。进程是程序的一次执行,属于一种动态的概念。在多道程序环境下,程序的执行属于并发执行,此时它们将失去封闭性,并具有间断性,运行结果也将不可再现,为了能使多个程序可以并发执行,提高资源利用率和系统吞吐量,并且可以对并发执行的程序加以描述和控制,引入进程的概念。 -
进程和线程
线程的引入主要是为了减少程序在并发执行时所付出的时空开销。我们知道,为了能使程序能够并发执行,系统必须进行创建进程、撤销进程以及进程切换等操作,而进程作为一个资源的拥有者,在进行这些操作时必须为之付出较大的时空开销。
线程和进程的区别主要如下:(1) 进程是系统中拥有资源的一个基本单位,线程本身并不拥有系统资源,同一进程内的线程共享进程拥有的资源。(2) 进程仅是资源分配的基本单位,线程是调度和分派的基本单位。(3) 进程之间相对比较独立,彼此不会互相影响,而线程共享同一个进程下面的资源,可以互相通信影响。(4) 线程的并发性更高,可以启动多个线程执行同程序的不同部分。 -
并行和并发
并行是指两个或多个线程在同一时刻执行,并发是指两个或多个线程在 同一时间间隔 内发生。如果程序同时开启的线程数小于CPU的核数,那么不同进程的线程就可以分配给不同的CPU来运行,这就是并行,如果线程数多于CPU的核数,那就需要并发技术。
二、Java多线程
Java虚拟机允许应用程序并发地运行多个执行线程,常见的开启新的线程的方法主要有4种。
- (常用)任务类实现Runnable接口,在方法Run()里定义任务。
public class Main {
public static void main(String[] args) {
//将ThreadNew实例作为参数实例化Thread之后start启动线程
//Thread构造器接收Runnable接口实例
new Thread(new ThreadNew()).start();
System.out.println(" Thread Main ");
}
}
// 实现Runnable接口并在方法run里定义任务
class ThreadNew implements Runnable {
@Override
public void run() {
try { // 延时0.5秒
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New ");
}
}
- 任务类集成Thread,重写run()方法
public class Main {
public static void main(String[] args) {
new ThreadNew2().start();
System.out.println(" Thread Main ");
}
}
// 继承自类Thread并重写run方法
class ThreadNew2 extends Thread {
@Override
public void run() {
try { // 延时0.5秒
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New2 ");
}
}
- 实现接口Callable并在call()方法里得到线程执行结果。
public class Main {
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(new ThreadNew3());
new Thread(futureTask).start();
System.out.println(" Thread Main ");
try {
System.out.println("执行结果是 " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 实现接口Callable并在call()方法里定义任务
class ThreadNew3 implements Callable<String> {
@Override
public String call() throws Exception {
try { // 延时0.5秒
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" Thread New3 ");
return "Thread New3 Result";
}
}
- 通过线程池创建线程
上面4种就是Java中开启新的线程的方式,其中第1种,实现Runnable接口最常用,也最灵活,第2种,因为任务类必须继承自Thread,而Java中又仅支持单继承,所以有时不太方便,第3种方法主要是可以得到线程执行的返回结果。
开启的新线程都有一个线程优先级,代表该线程的重要程度,可以通过Thread类的getPriority()和setPriority()来得到或者设置线程的优先级。线程的优先级范围是1~10,默认情况下是5。
在线程创建完成还未启动的时候,我们可以通过方法setDaemon()来将线程设置为守护线程。守护线程,简单理解为后台运行线程,比如当程序运行时播放背景音乐。守护线程与普通线程在写法上基本没有区别,需要注意的是,当进程中所有非守护线程已经结束或者退出的时候,即使还有守护线程在运行,进程仍然将结束。
- 终止线程
Java没有提供任何机制来安全地终止线程,那么怎么使线程停止或者中断呢?
- 线程自己在run()方法执行完后自动终止(安全的方式)
- 调用Thread.stop()方法强迫停止一个线程,不过此方法是不安全的,已经不再建议使用。(不安全方式)
- 比较安全可靠的是利用Java的中断机制,使用方法Thread.interrupt()。需要注意的是,通过中断并不能直接终止另一个线程,需要被中断的线程自己处理中断。被终止的线程一定要添加代码对isInterrupted状态进行处理,否则即使代码是死循环的情况下,线程也将永远不会结束。(安全方式)
三、锁机制
-
synchronized 同步锁
synchronized,是Java里面的一个关键词,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。用法如下:
写法一、修饰在方法上
public synchronized void add1() {
}
写法二、修饰在代码块上
public void add2() {
//这里的this指的是执行这段代码的对象
synchronized (this) {
}
}
写法三、指定一个小的对象值进行加锁
private byte[] lock = new byte[1];
public void add3() {
synchronized (lock) {
}
}
上面synchronized三种写法中,最后一种性能和执行效率最高,synchronized修饰方法上的效率最低。原因主要是作用在方法体上的话,即使获得了锁那么进入方法体内分配资源还是需要一定时间的。前两种锁的对象都是对象本身,加锁和释放锁都需要此对象的资源,那么自己造一个byte对象,可以提升效率。
关于sychronized的详细用法,可以查看这篇博文
-
ReentrantLock
在介绍ReentrantLock之前,我们先看一个接口Lock。
Lock提供比synchronized更丰富,更灵活的锁操作。Lock的实现类比synchronized更灵活,但是必须手动释放和开启锁,适用于代码块锁,synchronized对象之间是互斥关系。
ReentrantLock是接口Lock的一个具体实现类。当许多线程视图访问ReentrantLock保护的共享资源时,JVM将花费较少的时间来调度线程,用更多的时间执行线程。它的用法主要如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock(); // 开启锁
try {
//方法体
} finally {
lock.unlock();//释放锁
}
}
}
-
volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
下面我们看一下这个例子:
public class Counter {
public volatile static int count = 0;
public static void inc() {
//这里延迟1毫秒,使得结果明显
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行i++计算,看看实际结果
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
//这里每次运行的值都有可能不同,可能为1000
System.out.println("运行结果:Counter.count=" + Counter.count);
}
}
许多人认为加入volatile关键字之后,我们得到的最终值会是1000,但实际上为Counter.count=992。
volatile的应用场景 https://blog.csdn.net/vking_wang/article/details/9982709
为什么会出现这种情况呢?
我们知道,在jvm中,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
这里可以用AtomicInteger来声明count,它通过CAS算法保证了线程的安全性
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
但是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
四、线程池
Java通过Excutor提供4种线程池,分别为:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
1. newCachedThreadPool
创建一个可缓存(可扩展)线程池,如果线程长度超过处理需求,可灵活回收空闲线程,若无可回收的,则新建线程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
从jconsole中,我们可以看到线程数后来在程序运行中维持不变
活动线程数.JPG
2. newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。
3. newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大
4. newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
结果依次输出,相当于顺序执行各个任务。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。
为什么要使用线程池:
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类
ExecutorService: 真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。