Java多线程
多线程流程如图所示:
1.1. 进程
在操作系统中,进程是程序的一次执行。比如当双击某个可执行文件后,系统就创建一个进程专门执行这个程序的代码,在执行过程中,进程会申请、持有或释放操作系统资源(文件、内存等)。
操作系统调度进程的一种方法是时间片轮询算法(Round-robin scheduling)
1 分配给当前正在执行的进程一个很短的时间片,当前进程用完时间片后就被暂停,并被放入就绪队列。
2 然后操作系统从就绪队列中以轮询的方式随机选择一个进程来执行。轮询方式可使调度尽可能公平,但轮询是不严格的,各进程被调度的次数并不一样。
另外,如果正在执行的进程申请了某个资源,但没有立即得到这个资源,进程就会暂停执行,并被放入阻塞队列。获得资源后,这个进程会被加入到就绪队列等待调度
由于计算机运算速度非常快,分配给进程的时间片又非常短,给人的感觉就好像在某一时刻,多个进程(程序)在同时执行
在操作系统发展早期,进程是资源分配、调度、执行的基本单位。但由于进程持有系统资源等,调度时系统开销很大,于是便出现了轻量级进程——线程 。
1.2. 线程
线程和进程非常相似,又被称为轻量级进程。一个进程可拥有多个线程,这些线程共享此进程所持有的系统资源
现代操作系统中,调度、执行的基本单位变成了线程,进程则还是资源分配的基本单位。由于线程本身几乎不持有系统资源,在调度时系统开销就很小(但不同进程间的线程调度时,系统开销仍然很大)。
操作系统可以拥有多个进程,感觉就像多个程序同时在执行;进程可以拥有多个线程,感觉就像一个程序可以同时做多件事情
线程与进程的区别(来自维基百科)
Threads differ from traditional multitasking operating system processes in that:
-
processes are typically independent, while threads exist as subsets of a process
-
processes carry considerably more state information than threads, whereas multiple threads within a process share process state as well as memory and other resources
-
processes have separate address spaces, whereas threads share their address space
-
processes interact only through system-provided inter-process communication mechanisms
-
context switching between threads in the same process is typically faster than context switching between processes.
-
进程通常是独立的,而线程作为进程的子集存在
-
进程具有单独的地址空间,而线程共享其地址空间
-
进程只能通过系统提供的进程间通信机制进行交互
-
同一进程中线程之间的上下文切换通常比进程之间的上下文切换更快。
1.3. Thread
在java语言中我们可以通过操作Thread类用来创建线程对象,编写线程执行的代码、控制线程等
public class ThreadTest{ public static void main(String[] args){ // TODO Auto-generated method stub MyThread myThread = new MyThread(); myThread.start();// 把myThread加入到就绪队列里面 } } class MyThread extends Thread{ @Override public void run(){ // TODO Auto-generated method stub System.out.println("MyThread执行了"); } }
1.4. 线程状态
线程的整个生命周期过程可能会经过多次状态转变:
Ø 初始状态:创建了一个线程对象
Ø 就绪状态:线程具备运行的所有条件,在就绪队列中,在等待操作系统调度
Ø 运行状态:线程正被CPU处理
Ø 阻塞状态:线程在等待一个事件,逻辑上不可执行,在阻塞队列中
Ø 结束状态:线程执行结束,也就是run方法执行结束
状态流程图如下:
1.5. Runnable接口
除了继承Thread重写run方法外,在简单的情况下,还可通过实现Runnable接口的方式编写线程执行的代码
Thread thread = **new** Thread(**new** Runnable() {`
@Override
public void run() {
System.**out**.println("Runnable接口方式实现多线程");
}
});
1.6. sleep
Thread.sleep(time)会让执行该句代码的线程立即睡眠(挂起、阻塞)指定的时间,但该线程并不释放所持有的锁。在指定时间过去后,该线程就会从阻塞队列中转移到就绪队列
sleep方法可以降低程序执行速度,一般用在那些不希望太快的情况下
public class SleepTest {
public static void main(String[] args) {
System.out.println("倒计时开始");
for (int i = 10; i > 0; i--) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
}
1.7. 线程安全问题
一个数据,如一个对象或对象中的某个字段,如果有多个线程可以同时访问它,就可能会出现线程安全问题:数据错乱、程序出错或其他无法预知的问题
比如线程1要遍历一个list,线程2要把这个list清空,如果这两个线程同时执行就可能会出现线程安全问题
public static void main(String[] args){
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
list.clear();
}
});
thread1.start();
thread2.start();
}
执行结果均为null...
易错点:
1 计算机执行速度太快,如果循环次数太少就可能看不到想要的效果
2 匿名类访问外部变量时,如果使用的是jdk8以下的版本,就需要使用final修饰该变量
1.8. 同步代码块
线程同步控制,即使用某种方式使得一个线程在操作完某个数据前,别的线程无法操作这个数据,从而避免多个线程同时操作一个数据,进而避免线程安全问题。线程同步控制的方式有同步锁机制、等待/通知机制、信号量机制等,它们应用在不同复杂度的场景下
synchronized**同步锁机制**
Java中每个对象都有一把锁,同一时刻只能有一个线程持有这把锁。线程可以使用synchronized关键字向系统申请某个对象的锁,得到锁之后,别的线程再申请该锁时,就只能等待。持有锁的线程在这次操作完成后,可以释放锁,以便其他线程可以获得锁
synchronized有两种形式,synchronized代码块和synchronized方法
synchronized**代码块**,又称同步代码块:
public static void main(String[] args)
{
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
synchronized (list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
synchronized (list) {
list.clear();
}
}
});
thread1.start();//由于时间片轮询算法不一定会先启用thread1线程
thread2.start();
}
}
1.9. 同步方法
synchronized**方法**,又称同步方法:
public static void main(String[] args){
Data data = new Data();
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
data.bianliList();
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
data.clearList();
}
});
thread1.start();
thread2.start();
}
}
class Data{
private List<Integer> list;
public Data(){
list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
}
// 同步方法
public synchronized void bianliList(){
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public synchronized void clearList(){
list.clear();
}
}
非静态同步方法申请的锁是类的当前对象的锁,静态同步方法申请的锁是类的Class对象的锁。同步方法执行完后即向系统归还锁
synchronized代码块和synchronized方法的效果一样,可根据具体场景灵活选用
对于简单的需要线程同步控制的应用场景,synchronized基本够用。但需要注意,所有需要同步的线程必须都申请同一个对象的锁,当申请不同的锁或者有的线程没有使用synchronized时,同步锁机制就会失效。
1.10. wait/notify 等待/通知机制
对于稍复杂的情况,比如多个线程需要相互合作有规律的访问共享数据,就可以使用wait/notify机制,即等待/通知机制,也称等待/唤醒机制
等待/通知机制建立在synchronized同步锁机制的基础上,即在同步代码块(或同步方法)内,如果当前线程执行了lockObject.wait()
(lockObject表示提供锁的对象),则当前线程立即暂停执行,并被放入阻塞队列,并向系统归还所持有的锁,并在lockObject
上等待,直到别的线程调用lockObject.notify()
。
如果有多个线程在同一个对象上等待,notify()
方法只会随机通知一个等待的线程,也可以使用notifyAll()
方法通知所有等待的线程。被通知的线程获得锁后会进入就绪队列
public static void main(String[] args)
{
Object lockObject = new Object();
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
synchronized (lockObject) {
try {
System.out.println("线程1即将开始在lockObject上等待");
lockObject.wait();
System.out.println("线程1收到通知并获得锁,开始继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
synchronized (lockObject) {
System.out.println("线程2将随机通知在lockObject上等待的线程");
lockObject.notify();
}
}
});
thread1.start();
thread2.start();
}
1.11. 生产者消费者
一个很典型的生产者消费者例子:现有一个生产者、一个消费者、10个盘子(缓冲区),生产者把生产的产品放入空盘子中,当没有空盘子时就停止生产;消费者消费盘子中的产品,当所有的盘子都是空盘子时就停止消费
public static void main(String[] args){
List<Integer> buffer = new LinkedList<Integer>();
int maxSize = 10;
Producer producer = new Producer(buffer, maxSize);
Consumer consumer = new Consumer(buffer);
producer.start();
consumer.start();
}
}
// 模拟生产者
class Producer extends Thread{
private List<Integer> buffer; // 缓冲区,表示多个盘子
private int maxSize; // 表示盘子个数
public Producer(List<Integer> buffer, int maxSize)
{
this.buffer = buffer;
this.maxSize = maxSize;
}
@Override
public void run()
{
int id = 0;
while (true) {
synchronized (buffer) {
if (buffer.size() < maxSize) {// 有空盘子则继续生产
id++;// 表示生产了一个产品
buffer.add(id); // 表示把产品放入一个空盘子中
System.out.println("生产产品" + id + "并通知消费者可以消费了");
buffer.notify(); // 通知消费者有产品可以消费了
} else {
// 如果没有空盘子则等待
System.out.println("没有空盘子了,生产者停止生产产品");
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
// 模拟消费者
class Consumer extends Thread
{
private List<Integer> buffer; // 缓冲区,表示多个盘子
public Consumer(List<Integer> buffer)
{
this.buffer = buffer;
}
@Override
public void run()
{
while (true) {
synchronized (buffer) {
if (buffer.size() > 0) { // 有不空的盘子
int id = buffer.remove(0); // 表示消费了一个产品
System.out.println("消费产品" + id + "并通知生产者有空盘了");
buffer.notify();
} else {
// 全部都是空盘子则等待
System.out.println("全部都是空盘子,消费者停止消费产品");
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
1.12. 信号量机制
典型应用场景如:
系统有资源A,资源B
线程1需要同时拥有资源A和资源B才能工作,
线程2需要同时拥有资源A和资源B才能工作,
在进行同步控制时有可能出现这种情况:线程1拥有资源A,线程2拥有资源B,两个线程相互等待对方先释放资源,并会一直这么僵持下去,这种情况称为死锁。
为了避免死锁,可以使用信号量机制:线程在尝试申请某个资源前都要判断能否一次性就获得所有需要的资源,如果能,就申请,如果不能,则不申请,一直等到可以一次性获得所有资源。(操作系统底层实现就是采用这种方式)。
1.13. 守护线程
守护线程是一种特别的线程,主要为同一进程中的其他线程提供服务,而且经常使用无限循环持续提供服务。
当所属的进程中所有的非守护线程都执行完时,所有的守护线程也随即结束,整个进程也将结束(调用System.exit()
可使程序直接结束)
在调用start()
前调用setDaemon(true)
可将一个线程设置为守护线程
public static void main(String[] args){
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
while (true) {
System.out.println("守护线程主要是提供某种服务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread1.setDaemon(true);
thread1.start();
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread2.start();
}
}
1.14. interrupt
如果线程2可以访问线程1对象本身,就可以通过调用线程1对象的interrupt()方法来打断处于阻塞状态(sleep、wait)的线程1,使之从阻塞队列转移到就绪队列(sleep)或者转移到另外的阻塞队列(wait)。
public class InterruptTest{
public static void main(String[] args){
Thread thread1 = new Thread(new Runnable(){
@Override
public void run(){
System.out.println("线程1即将进入阻塞状态");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程1被打断,开始执行catch里面的代码");
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
});
thread1.start();
thread2.start();
}
1.15. 线程优先级
可以通过调用线程对象的setPriority()
方法设置线程的优先级,但在采用时间片轮询算法的操作系统中线程优先级的效果不明显。
public class PriorityTest{
public static void main(String[] args){
MyThread2 thread0 = new MyThread2();
MyThread2 thread1 = new MyThread2();
MyThread2 thread2 = new MyThread2();
MyThread2 thread3 = new MyThread2();
MyThread2 thread4 = new MyThread2();
thread4.setPriority(Thread.MAX_PRIORITY);
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class MyThread2 extends Thread{
@Override
public void run(){
for (int i = 0; i < 2000; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "执行完了");
}
}