Java基础自学第七期——并发
当我们打开电脑的时候,我们可以一边听音乐,一边聊微信,一边浏览网页,好像我们电脑可以同时运行多个程序。现在我们电脑可能都是多个CPU,四核八线程,六核十二线程等。但是并发执行的进程数目并不是由CPU的数组制约的。这依赖于操作系统将CPU的时间片分配个每一个进程,给人并行处理的感觉。
接下来我们介绍一下线程,进程,并发,并行的区别吧。
一、基础部分:
并行:
是指计算机在同一时刻运行多个任务。就好像你写作业的同时还在听音乐,两个任务是同时进行的。
并发:
是指计算机在同一段时间内运行多个任务,但同一时刻只有一个程序在CPU上运行。就好像你正在写作业,突然你同学让你打游戏,这是你暂时放弃写作业,等打完游戏你再继续原来的写作业进度。到最终写完作业这段时间内,你同一时刻要么在写作业,要么在打游戏,但在一段时间内完成了多个任务。
这时候你可能会问,为什么计算机在并发处理的时候感觉像是并行处理。因为操作系统会给CPU分配时间片,在这个时间片内CPU只能处理一个任务,但只要时间片足够小,我们就体会不到程序在CPU中切换,所以我们就会觉得多个程序在计算机上好像是一起运行的。
进程:
进程其实就是一个正在运行的程序。例如一个播放音乐的播放器,一个运行的游戏等。
线程:
线程是存在于进程内,是运行程序的一条路径。一个进程可以有一个或多个线程。
打个比方,我们流水线工厂中,每个流水线上会有很多的工人。这里我们每个流水线就充当着一个进程,而工人们就是线程。每个流水线拥有着自己独立的资源,而多个线程则是共享进程的资源。
二、线程的创建和使用
通过继承Thread类创建
Java的JVM允许程序运行多个线程,通过java.util.Thread类体现。
构造器分别有两种:
//无参构造,默认构造方法
public Thread1() {
super();
}
//参数为线程的名称,可以通过调用线程的toString方法打印名称
public Thread1(String name) {
super(name);
}
使用多线程有以下几个步骤:
- 创建Thread类的子类
- 子类中重写run方法,设置线程的任务
- 创建子类对象
- 调用对象的start方法(运行子类中的run方法)
多个线程分别创建了不同的栈空间来执行,互不影响
多线程采用抢占式调度,谁先抢到CPU,谁先运行,运行后释放CPU,再继续抢。
想调用多线程必须使用start方法,调用run方法只是普通调用
//子类中重写的run方法,并打印出是哪个线程正在运行
public class Thread1 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++)
System.out.println(i+this.getName());
}
}
//主函数创建了两个线程
public class Launch {
public static void main(String[] args) {
Thread1 th1 = new Thread1();
Thread1 th2 = new Thread1();
th1.start();
th2.start();
}
}
打印结果中既有Thread-0也有Thread-1,而不是把Thread-0打印100遍,再把Thread-1打印100遍。因为我们这两个线程对于抢占CPU的优先级是平等的。大家争抢互有胜负,才会出现这种交替运行的情况。
通过实现Runnable接口创建
我们打开IDEA可以看到Thread类是实现了Runnable接口的。
public interface Runnable {
public abstract void run();
}
接口中只有run方法。
这里的构造器也有两种
//创建一个实现了Runnable接口的指定线程,并指定了线程的名称
public Thread1(Runnable target, String name) {
super(target, name);
}
//创建一个实现了Runnable接口的指定线程
public Thread1(Runnable target) {
super(target);
}
步骤:
- 创建Runnable接口的实现类
- 实现类中重写run方法
- 创建实现类对象
- 创建Thread类对象,在构造方法中传递实现类对象
- 调用线程对象的start方法
可以看出,由于我们Thread类本身就已经实现了Runnable接口,所以Thread类也是Runnable的一个实现类。
我们也可以通过匿名内部类来实现现成的创建:
public static void main(String[] args) {
Thread1 th1 = new Thread1("Thread1");
Thread1 th2 = new Thread1("Thread2");
th1.start();
th2.start();
Thread th3 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
},"thread3");
th3.start();
}
Thread类和Runnable类创建线程的区别
推荐使用Runnable类创建:
- 避免了单继承的局限性,创建的Thread子类只能继承Thread一个父类。而Runnable的实现类则可以继承其他的父类。
- 增强了程序的扩展性,降低了耦合性。使用第二种方法,将创建线程任务和开启新线程分离,我们可以通过传递不同的实现类对象,来实现不同的run方法。
线程的优先级
如果我们想提高某个线程抢占CPU的几率,我们可以改变该线程的优先级:
Thread1 th1 = new Thread1("Thread1");
th1.getPriority(); //获取线程的优先级
th1.setPriority(10); //改变线程的优先级
线程中的几种常用方法
void start(): 启动线程,并执行对象的run()方法
void run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void sleep(long millis):(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到继续争夺CPU。
线程的几种状态
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。例如被睡眠或者等待
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步
线程同步就是指多个线程同时访问一段代码,访问了共享资源,会导致一些意想不到的错误。我们假设多线程运行相互独立,谁也不知道对方线程在做什么。例如,我们在去买包子的时候,一共就十个包子,我买走五个包子,准备打电话告诉同事还有五个包子。另一个人在我打电话时把剩余的包子买走了,同事一过来傻眼了,没包子了。
这个例子就说明,两个线程同时修改了公共资源,会导致程序出错。原因就是当多个线程在操作同一段代码共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
为了解决这个问题,我们可以对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Synchronized的使用方法
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。Synchronized就是扮演了这样的角色。
1.Synchronized可以同步代码块:
synchronized (参数){
// 需要被同步的代码;
}
2.Synchronized可以修饰一个方法作为同步代码块:
public synchronized void show (){
//方法体
}
被Synchronized修饰的方法和代码块,同一时刻只能有一个线程访问
Lock
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的ReentrantLock,可以显式加锁、释放锁。
public class ReLock{
Lock lock = new ReentrantLock();
public void method(){
lock.lock();
//方法体
lock.unlock();
}
}
ReentrantLock是Lock的一个实现类,与synchronized有相同的并发性。Lock的使用需要我们手动上锁和解锁。synchronized则在域内自动上锁,脱离了域则不会被锁住。
Lock和Synchronized区别
- Lock是显式锁,需要手动上锁解锁。Synchronized是隐式锁,出了作用域会自动释放
- Lock只能修饰代码块,Synchronized可以修饰代码块和方法
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
线程的通信
多线程处理同一资源,但任务不相同。它们能相互联系,有规律执行。
wait()、notify()和notifyAll()
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll():唤醒正在排队等待资源的所有线程结束等待
以上的三个方法只能在Synchronized中使用
线程之间的通信有一个经典的生产者消费者问题,大家可以去看一下,形象地描述了线程之间的通信。
线程池
当我们要大量使用线程时,我们可以创建线程池。线程池就是容纳多个线程的容器。
- 先使用Executors静态方法创建线程池
- 创建Runnable实现类,重写run
- 调用FixedThreadPool()返回的ExecutorService实现类对象的submit方法
- 在submit方法中创建Runnable匿名内部类
- 调用start方法
- shutdown方法销毁线程池