文章目录
一、线程概念
1、并发与并行
并发:指两个或多个事件在同段时间内发生。
并行:指两个或多个事件在同一时刻同时发生。
并行比并发效率高。
注意:
单核处理器的计算机肯定是不能并行处理多个任务,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行的,但从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,把这个情况称之为线程调度。
2、线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中可以有多个线程,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程至少有一个线程。多线程效率高,多个线程之间互不影响。
3、线程调度
-
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
-
抢占式调度
优先让优先级高的线程使用CPU,如果线程优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- 可在任务管理器中详细信息一栏设置线程的优先级
- CPU在多个线程中高速切换。优先级高的线程使用CPU的几率就大很多,使用时间也会多很多。如果CPU是多核多线程的,那么比CPU单核单线程的处理线程效率高。
二、线程
Thread、Runnable、Callable
3种创建方式:
- Thread 类 — 继承Thread类
- Runnable接口 — 实现Runnable接口
- Callable接口 — 实现Callable接口
注意:
thread不建议使用,因为OOP单继承的局限性。
推荐使用实现runnable接口的方式,创建线程,避免OOP单继承的局限性、灵活方便、方便一个对象被多个线程调用。
Java中默认有2个线程:main线程、GC(垃圾回收)线程
1、主线程
在Java中,main方法就是一个主线程。
单线程程序:java程序中只有一个线程执行main方法开始,从上往下依次执行。当该线程中有一处发生异常,那么该线程就会终止,就不会往后继续执行。
JVM执行main方法,main方法会进入栈内存,JVM会找操作系统开辟一条main方法通向CPU的执行路劲,CPU可通过该路径执行main方法,而这个路径有一个名字叫mian(主)线程
public class Test{
public static void main(String[] args){
Person p1 = new Person("xi");
p1.run();
System.out.println(0/0); // 在这里会抛出ArithmeticException,就会终止程序,后面代码不会执行
Person p2 = new Person("wa");
p2.run();
}
}
2、多线程原理
当线程对象创建之后,调用start方法,就会启动新的一个线程,JVM就会通知OS给新的线程开辟一条路径,main线程和新的线程同时会抢夺一个CPU,CPU的调度是不能人为干预,都是由CPU随机调取其中一个线程来处理,在两个线程之间来回高速切换。
多线程内存原理
执行main线程,main方法就会进入到栈内存中,main线程开始执行,第一行代码new了一个线程对象,那么该对象就会存在推内存中;下一行代码调用了该对象的run方法,那么就会将run方法引入到主线程的栈内存中执行;下一行代码调用了start方法,那么就会开辟一个新的栈内存,该栈内存就引入该线程对象里的run方法,在新的栈内存中执行,也就是开启了新的一个线程。CPU就会在多个线程之间进行随机挑选,来回高速切换执行。
简而言之,只要线程对象调用了start方法,就会开辟新的栈内存空间,执行该线程对象的run方法,栈内存之间的执行互不影响,相互独立。
run()与start() 区别
线程不一定立即执行,由CPU调度安排。
3、Thread类
java.lang.Thread
类实现了Runnable接口,代表线程,所有的线程对象都必须是Thread类或其子类的实例或者可以让Thread类做代理。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。Java使用线程执行体来代表这段程序流。
构造方法
常用
public Thread()
分配一个新的Thread对象public Thread(String name)
分配一个指定名字的Thread对象public Thread(Runnable target)
分配一个带有指定目标新的Thread对象public Thread(Runnable target, String name)
分配一个带有指定目标并为其指定名字的Thread对象
常用方法
public String getName()
获取线程的名称public void setName()
改变线程的名称public void start()
导致此线程开始执行; Java虚拟机调用此线程的run
方法。线程只能启动一次,多次启动非法。public void run()
此线程要执行的任务代码public static void sleep(long millis) throws InterruptedException
使当前正在运行的线程暂时停止执行的毫秒数。报中断异常public static Thread currentThread()
返回对当前正在执行的线程对象的引用public static void yield()
线程礼让,退出线程,进入可运行状态,并且线程调度的时候也可能会被cpu选中。yield()方法会通知线程调度器放弃对处理器的占用,并不会释放锁,但调度器可以忽视这个通知。yield()方法主要是为了保障线程间调度的连续性,防止某个线程一直长时间占用cpu资源。但是他的使用应该基于详细的分析和测试。这个方法一般不推荐使用,它主要用于debug和测试程序,用来减少bug以及对于并发程序结构的设计。(可释放cpu资源,不释放锁)public final void join() throws InterruptedException
强行插队,线程调用这个方法,就会先执行该线程,并该线程死亡了其他线程才能继续执行。
Java通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写
run()
方法,该run()
方法的方法体就是代表了线程需要完成的任务,因此run()
方法称为线程执行体; - 创建Thread子类的实例,即创建了线程对象;
- 调用线程对象的
start()
方法来启动该线程。
// 使用继承Thread类,通过实例调用start方法开启一个新的线程
public class ThreadTest extends Thread {
public ThreadTest() {
}
public ThreadTest(String name) {
super(name); // 将给线程设置的名称传给父类进行设置
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + i + "次");
}
}
public static void main(String[] args) {
// ThreadTest threadTest = new ThreadTest("heroC");
ThreadTest threadTest = new ThreadTest();
threadTest.setName("heroC"); // 给该线程设置名称
threadTest.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程执行" + i + "次");
}
System.out.println(Thread.currentThread().getName());// 输出 main;获取当前线程的名称。Thread.currentThread()获取到了当前线程的对象,然后在调用getName()获取该线程的名称
}
}
练习:通过继承线程类,开启多线程下载网络图片
该练习,要使用到FileUtils类,该类为io工具类,是apache旗下的。
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class DownPricFileUtis extends Thread{
private String url;
private String name;
public DownPricFileUtis(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
Downloader d1 = new Downloader();
d1.downLoader(url,name);
System.out.println(name + "已下载完成!");
}
public static void main(String[] args) {
DownPricFileUtis downP1 = new DownPricFileUtis("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582892937486&di=1f14ad0c117a0695ff8d4701d91a1b9f&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D637719543%2C1600461480%26fm%3D214%26gp%3D0.jpg","1.jpg");
DownPricFileUtis downP2 = new DownPricFileUtis("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1858174317,3137906118&fm=26&gp=0.jpg","2.jpg");
System.out.println("正在下载,请稍后...");
// 开启2条线程下载,会自动执行重写的run方法
downP1.start();
downP2.start();
}
}
class Downloader{
public void downLoader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
System.out.println("IO异常,downLoader方法");
}
}
}
4、Runable接口
java.lang.Runnable
创建一个线程是声明实现类Runnable
接口。该接口只有一个run
方法。
推荐使用Runnable对象,因为Java单继承的局限性
实现线程步骤:
- 定义Runnable接口的实现类,并重写该接口的run方法;
- 创建Runnable实现类的实例对象,并以此实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象;
- 调用线程的start方法来启动创建一个新的线程。
// 实现Runnable接口
public class Test implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 获取当前线程的引用之后,获取当前线程的名字
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
public static void main(String[] args) {
Test test = new Test();
// 通过Thread类将Runnable实现类传过去,并给线程命名,并启动
new Thread(test,"heroC").start();
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
5、Thread类 与 Runnable接口 的区别
Thread类实现了Runnable接口。如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的类,因为不是继承了Thread类,就不是真正的线程代码,则很多类都可以使用该类的代码,达到资源共享的目的。
【总结】
实现Runnable接口比继承Thread类所具有的优势:
-
适合多个相同的程序代码的线程去共享同一个资源;
(如果多个程序需要同一个线程的代码,直接用Runnable实现的类去写run方法,然后不同程序将该类引用过去,new一个Thread方法将该类创建成一个线程执行run代码)
-
可以避免java中的单继承的局限性;
(很多时候当你继承了一个父类,你这个类又要使用Thread类,因不能多继承,所以无法实现相应功能,为了解决该问题,就可以使用线程代理Runnable来创建一个线程)
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立;
-
线程池只能放入实现Runanble或Callable类线程,不能直接放入继承Thread的类。
扩充:
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每个JVM其实是在操作系统中启动了一个进程。
6、匿名内部类实现线程创建
匿名内部类,就是没有名字的类。只是把类的方法,提到了new对象的后面来写了,直接使用接口或者父类来创建一个匿名的类。
格式1(使用有参构造方法)避免错,推荐使用格式:
new 父类/接口(构造方法的参数){
重写方法/实现方法
};
格式2(使用无参构造方法):
new 父类( () - > {
重写方法/实现方法
});
实战:
public class NoNameTest {
public static void main(String[] args) {
// 创建了一个匿名类,该匿名类继承了Thread类,并重写了run方法,调用了star方法启动线程
new Thread("Thread"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}.start();
// 创建了一个匿名类,实现了Runnable接口,并重写了run方法,调用了start方法启动线程
/*Runnable runnable = new Runnable(){ // 多态,接口变量与一个接口的实现类
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
};*/
// 可简写成:
Runnable runnable = () -> { // 多态,接口变量与一个接口的实现类
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
};
new Thread(runnable,"Runnable").start();
// Runnable还可以简化:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
},"RunableSimple").start();
}
}
7、Callable<V> 接口
java.util.concurrent Interface Callable<V>
只有一个抽象类,V call() trows 异常
好处:
- 可以返回值
- 可以抛出异常
public class CallableTest implements Callable<Boolean> {
// 重写call方法
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName()+" Callable执行的线程...");
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);// 不建议这样创建线程池,会出现OOM,具体见JUC
// 提交线程,创建线程
Future<Boolean> s1 = executorService.submit(new CallableTest());
Future<Boolean> s2 = executorService.submit(new CallableTest());
Future<Boolean> s3 = executorService.submit(new CallableTest());
// 获取线程返回值
Boolean sr1 = s1.get();
Boolean sr2 = s2.get();
Boolean sr3 = s3.get();
// 关闭线程池服务
executorService.shutdownNow();
}
}
三、线程安全
1、线程安全
有多个线程在同时运行,而这些线程都是相互独立,互不影响的。假如多个线程同时买100张电影票,每个线程运行结果就如同单线程运行的结果一样,每个线程都会去卖这100张票,就会出现线程安全问题,多个线程会出现卖出同一张票的情况以及会出现卖出负数票的情况。
通过卖票案例,演示线程出现安全问题:
public class TicketSalesProblem implements Runnable{
// 有100张票
private int tickets = 100;
/*
执行买票操作
*/
@Override
public void run() {
while (tickets > 0){
// 为了提高安全问题的出现率,每次让线程暂停再执行,模拟处理器处理速度满的情况,
// 这时就会出现大量重复票和负数票
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("中断异常");
}
System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
}
}
public static void main(String[] args) {
// 一个Runnable实例对象,开启3个线程
TicketSalesProblem ticketSalesProblem = new TicketSalesProblem();
new Thread(ticketSalesProblem,"售票窗口A ").start();
new Thread(ticketSalesProblem,"售票窗口B ").start();
new Thread(ticketSalesProblem,"售票窗口C ").start();
}
}
// 该案例输出了大量重复的票和负数票
/*
...
售票窗口A 已售出第44号票
售票窗口B 已售出第44号票
售票窗口C 已售出第43号票
...
售票窗口C 已售出第2号票
售票窗口C 已售出第1号票
售票窗口B 已售出第0号票
售票窗口A 已售出第-1号票
...
*/
出现0号票和负数票的原因:
出现重复票的原因:
在执行过程中,该电脑是4核的,可并行执行线程,所以难免两个线程会同时执行输出语句,这时候tickets的值都是一样的,还没有发生变化,所以在这一时刻,会卖出同一个号码的票。
注意:
线程安全问题是不能产生的,解决方案是,当线程在访问共享资源的时候,无论该线程是否失去了CPU执行权,其他线程都必须等待,等待当前线程完成所有操作,其他线程才可以操作。只要有一个进程在操作该共享资源,其他线程都必须等待。保证,始终只有一个线程在执行共享资源代码就可。
2、线程同步
当我们使用多个线程访问同一资源时候,且多个线程中对资源有写的操作或更改的操作,就容易出现线程安全问题。要解决多线程并发访问一个资源的安全性问题,Java中提供了同步(synchronized)机制来解决。
根据卖票案例简述:
窗口A线程进入操作的时候,窗口B窗口C只能在外面等待,抽口A操作结束,抽口A抽口B抽口C才有机会抢夺CPU去执行代码。也就是说某一个进程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
有3种方式完成同操作:
- 同步代码块
- 同步方法
- 锁机制
1)同步代码块
使用关键字synchronized
的代码块,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象 可以是任意类型
- 多个线程对象 要使用同一把锁
注意:
在任何时候,最多允许一个线程有同步锁,谁拿到这个锁就可以进入代码块,其他线程只能在外面等着(BLOCKED)。
缺点:
程序频繁的判断锁,获取锁,释放锁,程序的效率会降低。
使用同步代码块解决线程安全:
public class SolveTicketSalesProblem implements Runnable{
// 有100张票
private int tickets = 100;
// 创建一个锁对象
Object object = new Object();
/*
执行买票操作
*/
@Override
public void run() {
while (true){
// 同步代码块
synchronized (object){
// 为了提高安全问题的出现率,每次让线程暂停再执行,模拟处理器处理速度满的情况
// 因为有同步代码块,锁对线程的标记,所以不会出现线程安全问题
// 一定要在锁里面判断票是否还有
if(tickets > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("中断异常");
}
System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
}
}
}
}
public static void main(String[] args) {
SolveTicketSalesProblem salesProblem = new SolveTicketSalesProblem();
new Thread(salesProblem,"售票窗口A").start();
new Thread(salesProblem,"售票窗口B").start();
new Thread(salesProblem,"售票窗口C").start();
}
}
同步代码块的原理
多个线程执行一个共享资源,当某个线程抢到了CPU执行权就会进入run方法开始执行,当执行到synchronized同步代码块时,就会检查有没有锁对象,如果有锁对象,就会携带者锁对象进入到同步代码块进行执行,如果没有锁对象,那么就会在同步代码块外便阻塞等待,直到其他线程释放了锁对象,该线程拿到锁对象为止。所以,同步代码块中,永远只能一个线程在执行。
总结:同步中的线程,没有执行完不会释放锁对象,同步外的线程没有锁对象进不去同步。
2)同步方法
使用关键字synchronized
修饰的方法,叫做同步方法,保证A线程执行该方法的时候,其他线程都在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全的代码
}
同步方法的同步锁是谁?
如果是static修饰的方法,使用的是当前方法所在类的字节码对象(类.class)
如果非static修饰的方法,同步锁就是使用了this对象(多个线程都是操作的一个对象,所以this对象也是唯一的)
使用同步方法代码如下:
public class SolveTicketSalesProblem2 implements Runnable {
// private int tickets = 100;
private static int tickets = 100;
@Override
public void run() {
while (true){
// buyTickets();
buyTicketsStatic();
}
}
// 非静态方法 同步锁就是给该类实例化的对象, this
// 有锁对象的线程才能执行该同步方法,否则在该同步方法外等待
/*public synchronized void buyTickets(){
if(tickets > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("中断异常");
}
System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
}
}*/
// 静态方法 同步锁是本类的class属性,就是class文件
// 因为静态方法创建与类同步的,早于实例之前,所以使用class文件对象作为锁对象
public static synchronized void buyTicketsStatic(){
if(tickets > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("中断异常");
}
System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
}
}
public static void main(String[] args) {
SolveTicketSalesProblem2 salesProblem2 = new SolveTicketSalesProblem2();
new Thread(salesProblem2,"售票窗口A").start();
new Thread(salesProblem2,"售票窗口B").start();
new Thread(salesProblem2,"售票窗口C").start();
}
}
3)Lock 接口 JDK 1.5
java.util.concurrent.locks interface Lock
机制提供了比synchronized
代码块和synchronized
方法更为广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称之为同步锁。加锁和释放锁被方法化了,如下:
public void lock()
获得同步锁public void unlock()
释放同步锁
java.util.concurrent.locks.ReentrantLock
类实现了Lock接口。可通过该类使用lock方法和unlock方法。
使用如下:
public class SolveTicketSalesProblem3 implements Runnable{
private int tickets = 100;
Lock lock = new ReentrantLock(); // 创建一个锁对象(多态)
@Override
public void run() {
while (true){
lock.lock(); // 获取同步锁
if(tickets > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
} catch (InterruptedException e) {
System.out.println("中断异常");
}finally {
lock.unlock(); // 无论代码是否发生异常,都会执行finally,将锁释放
}
}
}
}
public static void main(String[] args) {
SolveTicketSalesProblem2 salesProblem2 = new SolveTicketSalesProblem2();
new Thread(salesProblem2,"售票窗口A").start();
new Thread(salesProblem2,"售票窗口B").start();
new Thread(salesProblem2,"售票窗口C").start();
}
}