首先我们要明白什么是进程,什么是线程?
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
什么是多线程?
- 1.多进程是指操作系统能同时运行多个任务(程序)。
- 2.多线程是指在同一程序中有多个顺序流在执行.
- 3.很多线程都是模拟出来的,真正的多线程指的是多个cpu,即多核,如服务器。
- 4.如果是模拟出来的多线程,即在同一个cpu的情况下,cpu只能执行一个程序或一行代码,因为切换的很快,所以有了同时执行的错觉。
- 5.在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
线程的生命周期为什么?
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
我们之前在多线程之前学习的多线程基础基本上都是单线程执行的
普通方法调用和多线程
多线程核心概念
实现多线程的几种方式:
- 继承Thread类:
首先,我们看JDK1.8的帮助文档对Thread类的描述:空线是一个线程在执行一个程序。java虚拟机的应用程序可以有多个同时执行的线程。 每一个线程都有一个优先权。具有更高优先级的线程在优先于较低优先级的线程中执行。每一个线程可能会或可能不会被标记为一个守护进程。在一些线程中运行的代码创建了一个新的Thread对象时,新线程的优先级被设置为等于创建线程的优先级,是守护线程的当且仅当创建线程是一个守护进程。
让我们来写一个例子:
public class TestThread extends Thread{
private String url;//文件地址
private String fileName;//文件名
public TestThread(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
@Override
public void run() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.DownloadPicture(url, fileName);
System.out.println("下载了文件:"+fileName);
}
public static void main(String[] args) {
TestThread testThread = new TestThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606911965336&di=b68b28cd8bd82094ddfd3f9ecfd380e4&imgtype=0&src=http%3A%2F%2Fimage.namedq.com%2Fuploads%2F20181006%2F19%2F1538823821-QrIiEnXmAv.jpg", "亚索");
TestThread testThread2 = new TestThread("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2106604486,285772919&fm=26&gp=0.jpg", "光辉");
testThread.start();
testThread2.start();
}
}
//创建一个下载文件的类
class WebDownLoader{
//创建一个下载文件的方法
public static void DownloadPicture(String url,String fileName) {
try {
FileUtils.copyURLToFile(new URL(url), new File(fileName));
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行后:发现在项目中会多出两个下载的文件。
序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,两条线程理论上就同时去下载两个文件。
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
- 实现Runnable接口
首先,让我们看看JDK1.8帮助文档对Runnable接口的描述:创建一个线程是声明一个类实现Runnable接口的其他方式。该类实现run方法。那类的一个实例可以分配,作为一个参数传递Thread时创建,并开始。此外,Runnable一类是主动的而不是子类Thread提供手段。一个类实现Runnable可以运行没有子Thread实例化一个Thread实例并将自身作为目标。在大多数情况下,这Runnable接口应该是如果你只打算重写run()方法并没有其他Thread方法。这是重要的因为方法重写,继承类不应该继承除非程序员打算修改或增强类的基本行为。
让我们来写一个例子:
/**
* 演示线程的停止:1.创建线程,然后一直让它运行,通过我们自定义的方法让线程停止
* 2.不建议使用JDK中的自带的API方法来让线程停止,因为可能会存在某些问题(官方推荐我们使用:自定义一个标志位来让某个线程进行停止)
*
*
*/
public class TestThreadStop implements Runnable{
private static boolean flag=true;
@Override
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName()+"-->循环");
}
}
void testflag(){
flag=false;
System.out.println("自定义线程停止");
}
public static void main(String[] args) {
TestThreadStop testThreadStop = new TestThreadStop();
new Thread(testThreadStop).start();
//主线程执行for循环
for (int i = 0; i < 1000; i++) {
if (i==900) {
//让我们自定义的线程停止
testThreadStop.testflag();
}
System.out.println("主线程执行--》"+i);
}
}
}
输出结果:
当自定义线程输出到达900时候,线程会根据标志位停止,线程执行完毕。主线程继续执行。
- 实现Callable接口
首先,让我们看看JDK1.8帮助文档对Callable接口的描述:返回结果的一个任务,并可能抛出异常。用户定义一个不带参数调用 call 的Callable接口类似于Runnable,,是专为其实例的类可能被另一个线程执行。然而,Runnable,不返回结果并不能抛出异常。
Executors类包含的实用方法与其他常见的形式转换为Callable类。
让我们来写一个例子:
public class TestCallable implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
System.out.println("继承Callable接口的线程"+Thread.currentThread().getName()+"执行");
return true;
}
public static void main(String[] args) {
//1.创建两个继承了Callable接口的类的线程对象
TestCallable testCallable = new TestCallable();
TestCallable testCallable1 = new TestCallable();
//2.创建线程池服务,线程池最大为2个线程
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//3.往线程池中提交线程,并获取到线程执行call方法结果
Future<Boolean> submit = newCachedThreadPool.submit(testCallable);
Future<Boolean> submit2 = newCachedThreadPool.submit(testCallable1);
//4.获取接口
try {
Boolean boolean1 = submit.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
Boolean boolean2 = submit2.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
输出结果:
那我们之前学习了继承Thread类和实现Runnable接口来创建线程,这两个方式的区别是什么?
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。所以Runnbale比Thread跟具有优势
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
线程状态转换(线程的生命周期)
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的调度
- 线程睡眠:Thread.sleep(long millis)方法
让我们来写一个例子:
public class TestThreadSleep {
private static int mun=10;
//模拟倒计时方法
public static void tenDown() throws InterruptedException{
while (mun>0) {
Thread.sleep(2000);
System.out.println(mun--);
}
}
//打印当前时间方法
private static void datedayin(){
while (true) {
Date date = new Date(System.currentTimeMillis());//获取当前时间
try {
Thread.sleep(1000);//睡眠一秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前时间是:"+new SimpleDateFormat("HH:mm:ss").format(date));
date = new Date(System.currentTimeMillis());//更新当前时间
}
}
public static void main(String[] args) {
//模拟倒计时
/*try {
tenDown();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
//打印当前时间
datedayin();
}
}
输出结果:
注意:wait和sleep的区别:Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
2.线程礼让:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
3.线程强制执行:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
让我们写一个例子:
/**
*
* 演示java线程中的join方法:使某个线程进行插队,直到这个线程执行完后,其他线程才能执行
*
*/
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("大哥线程来执行了:"+Thread.currentThread().getName()+"-->"+i);
}
}
public static void main(String[] args) {
//创建自定义线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程执行for循环,到200的时候 join方法让自定义线程进行插队,直到自定义线程执行完后,主线程才继续执行
//(如果电脑是多核的,有多个cpu,可能在200之前,两个线程会交替执行)
for (int i = 0; i < 500; i++) {
if (i==200) {
try {
thread.join(); //进行插队
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("执行main方法:-->"+i);
}
}
}
输出结果:
当主线程达到200的时候,自定义线程使用join方法进行插队,直到自定义线程执行完毕为止,主线程才继续执行。
3.调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
注意:每个线程都有默认的优先级。主线程的默认优先级Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
让我们来写一个例子:
/**
*
* 多线程的优先等级:1-10
* 1 最低
* 5 默认
* 10 最高
*/
public class 多线程的优先级 {
public static void main(String[] args) {
// TODO Auto-generated method stub
//获取多线程的优先级等级
//1.最高优先级
System.out.println(Thread.MAX_PRIORITY);//10
//2.默认优先级
System.out.println(Thread.NORM_PRIORITY);//5
//3.最低优先级
System.out.println(Thread.MIN_PRIORITY);//1
Thread t1=new projuct();//获取线程
Thread t2=new projuct();
t1.setName("T1");//修改线程名
t2.setName("T2");
//查看线程优先级,自定义的线程没有设置前都是默认的优先级 5
System.out.println(t1.getPriority());//5
System.out.println(t2.getPriority());//5
//设置优先级
t1.setPriority(4);
t2.setPriority(10);
//执行线程
t1.start();
t2.start();
}
}
//定义一个线程
class projuct extends Thread{
public void run(){
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
//获取当前的线程和名称
}
}
}
输出结果为:
会发现在抢夺CPU调度的过程中,T2线程因为优先级的原因抢夺的CPU时间片会多一些。
守护线程
让我们来写一个例子:
public class 学习守护线程 {
/**
* 守护线程
* 其他所有线程结束,则守护线程退出
* 守护线程一般都是无限执行的
*
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1=new guard();
t1.setName("t1");
t1.setDaemon(true);//设置t1为守护线程
//知道主线程结束 它才会结束
t1.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class guard extends Thread{
public void run(){
int i=0;
while(true){
i++;
System.out.println(Thread.currentThread().getName()+"-->"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
输出结果:
线程的同步
首先,我们需要理解一个概念:什么是线程的同步?线程的同步就是多个线程同时操作同一个资源。
首先,我们可以看一个多个线程同时操作一个共有资源的例子,会发现出现很多bug和漏洞:
public class TestTicket implements Runnable{
//总共有10张票
private int Ticket=10;
@Override
public void run() {
while (true) {
if (Ticket<=0) {
break;//当票被抢完的时候,就退出循环
}
try {
Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//Thread.currentThread():返回当前线程
System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就--
}
}
public static void main(String[] args) {
//创建线程
TestTicket testTicket = new TestTicket();
new Thread(testTicket,"小明").start();
new Thread(testTicket,"老师").start();
new Thread(testTicket,"黄牛党").start();
}
}
结束输出:
这肯定是不允许的,事实上是不允许几个同时抢到同一张票的,这时候出现了数据混乱。
这时候我们就可以使用synchronized关键字
我们在上面的例子中加入synchronized锁:
public class TestTicket implements Runnable{
//总共有10张票
private int Ticket=10;
@Override
public void run() {
synchronized (this) {
while (true) {
if (Ticket<=0) {
break;//当票被抢完的时候,就退出循环
}
try {
Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//Thread.currentThread():返回当前线程
System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就--
}
}
}
public static void main(String[] args) {
//创建线程
TestTicket testTicket = new TestTicket();
new Thread(testTicket,"小明").start();
new Thread(testTicket,"老师").start();
new Thread(testTicket,"黄牛党").start();
}
}
synchronized关键字有什么用?
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
3) 除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;
4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
什么是死锁?
简单来说:就是两个线程各有一把锁,但是又想要对面手中的锁,但是又不肯把自己的锁给对方,这时候程序就无法继续向下执行。
/**
* 演示死锁的概念:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中)
* 这就会造成死锁,程序无法向下执行。
* 或者说:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中,并且别的线程这时候也想
* 获取到我们手中对象的锁,这样就会僵持不下,程序无法向下执行,直接卡死)
*
*
*/
/**
* 演示:1.创建两个小孩对象,小孩1获取到玩具车对象的锁,这时候想去获取到小孩2手中玩具飞机对象的锁
* 2.小孩2获取到玩具飞机对象的锁,这时候象去获取到小孩1手中玩具车对象的锁
* 3.这就造成了死锁状态,程序无法向下执行了
*/
//创建玩具车类
class Car{
}
//创建玩具飞机类
class Aircraft{
}
public class TestDieLock extends Thread {
static Car car=new Car();
static Aircraft aircraft=new Aircraft();
private String child;//小孩名
public TestDieLock(String child) {
this.child = child;
}
public void run() {
try {
wanju();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void wanju() throws InterruptedException {
if (child.equals("小孩1")) {
synchronized (car) {
System.out.println("小孩1获取到了玩具车的对象锁");
Thread.sleep(1000);
synchronized (aircraft) {
System.out.println("小孩1还想获取玩具飞机的对象锁");
}
}
}else {
synchronized (aircraft) {
System.out.println("小孩2获取到了玩具飞机的对象锁");
Thread.sleep(1000);
synchronized (car) {
System.out.println("小孩2还想获取玩具车的对象锁");
}
}
}
}
public static void main(String[] args) {
TestDieLock testDieLock = new TestDieLock("小孩1");
TestDieLock testDieLock2 = new TestDieLock("小孩2");
testDieLock.start();
testDieLock.start();
}
}
Lock锁
让我们来写一个例子:
/**
* 在JDK5.0的时候推出了一个可重入锁:Lock,这个锁可以作用和对象锁相同,但是这个锁需要自己去加锁和解锁,通常我们使用try catch来让我们的方法或者代码块
* 进行锁机制
*
*/
public class TestLock implements Runnable{
int num=10;
//定义可重入锁对象
private static ReentrantLock reentrantlock=new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//调用可重入锁对象加锁方法,给下面代码块加锁
reentrantlock.lock();
if (num>10) {
System.out.println(num--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
break;
}
}finally {
//使用可重入锁,别忘记在finally中解锁
reentrantlock.unlock();
}
}
}
public static void main(String[] args) {
TestLock testLock = new TestLock();
new Thread(testLock).start();
new Thread(testLock).start();
new Thread(testLock).start();
}
}