一.基础知识
1.线程与进程
进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。
- 线程实际上是在进程基础上进一步划分,一个进程启动之后,里面的若干执行路径又可以划分为若干个线程。
2.线程调度
分时调度
- 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度
- 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程之间进行高速切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而CPU在多个线程之间切换速度相对我们感觉要快,看上去就是在同一时刻运行。其实,多线程并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率提高。
3.同步与异步
同步:排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
4.并发与并行
并发:指两个或多个事物在同一时间段内发生。
并行:指两个或多个事物在同一时刻发生(同时发生)。
二.Java线程:创建与启动
1.扩展java.lang.Thread类
此类中有个run()方法,应该注意其用法:public void run()
如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法; 否则,该方法不执行任何操作并返回。
Thread的子类应该重写该方法。
2.实现java.lang.Runnable接口
使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调 用对象的run方法。
3.有返回值的线程(Callable)
可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到 Callable 任务返回的Object了。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** * Java线程:线程池-有返回值的线程
*/ public class Test {
public static void main(String[] args)throwsExecutionException,InterruptedException {
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建两个有返回值的任务
Callable c1=new MyCallable("A");
Callable c2=new MyCallable("B");
//执行任务并获取Future对象
Future f1=pool.submit(c1);
Future f2=pool.submit(c2);
//从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>"+f1.get().toString());
System.out.println(">>>"+f2.get().toString());
//关闭线程池
pool.shutdown();
}
}
class MyCallable implements Callable{
private String oid;
MyCallable(String oid) {
this.oid = oid;
}
@Override
public Object call() throws Exception {
return oid+"任务返回的内容";
}
}
### 4.线程启动
在线程的Thread对象上调用start()方法,而不是run()或者别的方法。
在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真 正的线程。
在调用start()方法之后:发生了一系列复杂的事情——
启动新的执行线程(具有新的调用栈);
该线程从新状态转移到可运行状态;
当该线程获得机会执行时,其目标run()方法将运行。
注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。
5.线程常见问题
- 线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。
- 线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
- 获取当前线程的对象的方法是:Thread.currentThread();
- 在上面的代码中,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
- 当线程目标run()方法结束时该线程完成。
- 一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
- 线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。
众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有 保障的。
- 尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列而成一个一个队列的事实。
- 尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
三.线程的同步与锁
1.同步问题提出
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
例如:两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据
public class Foo {
private int x = 100;
public int getX() {
return x;
}
public int fix(int y) {
x = x - y;
return x;
}
}
public class FooRunnable implements Runnable {
private Foo foo =new Foo();
public static void main(String[] args) {
FooRunnable r = new FooRunnable();
Thread ta = new Thread(r,"Thread-A");
Thread tb = new Thread(r,"Thread-B");
ta.start();
tb.start();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
this.fix(30);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ " :当前foo对象的x值= " + foo.getX());
}
}
public int fix(int y) {
return foo.fix(y);
}
}
2.同步和锁定
锁的原理:
Java中每个对象都有一个内置锁。
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有以下几个要点:
1)只能同步方法,而不能同步变量和类;
2)每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6)线程睡眠时,它所持的任何锁都不会释放。
7)线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
3.线程死锁
死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。
还是看一个比较直观的死锁例子:
public class Deadlock {
private static class Resource{
public int value;
}
private Resource resourceA=new Resource();
private Resource resourceB=new Resource();
public int read(){
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value+resourceA.value;
}
}
}
public void write(int a,int b){
synchronized(resourceB){
synchronized (resourceA) {
resourceA.value=a;
resourceB.value=b;
}
}
}
}
假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。
实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。
四.线程池
1.介绍
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
2.优点
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
3.Java中的四种线程池
(1)缓存线程池
/**
* 缓存线程池.
* (长度无限制)
* 执行流程:
* 1\. 判断线程池是否存在空闲线程
* 2\. 存在则使用
* 3\. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
(2)定长线程池
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1\. 判断线程池是否存在空闲线程
* 2\. 存在则使用
* 3\. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4\. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
(3)单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
/**
* 单线程线程池.
* 执行流程:
* 1\. 判断线程池 的那个线程 是否空闲
* 2\. 空闲则使用
* 4\. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
(4)周期性任务定长线程池
public static void main(String[] args) {
/**
* 周期任务 定长线程池.
* 执行流程:
* 1\. 判断线程池是否存在空闲线程
* 2\. 存在则使用
* 3\. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4\. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .
*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1\. runnable类型的任务
* 参数2\. 时长数字
* 参数3\. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1\. runnable类型的任务
* 参数2\. 时长数字(延迟执行的时长)
* 参数3\. 周期时长(每次执行的间隔时间)
* 参数4\. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
}