线程
学习线程首先要明确几个概念:程序,进程,线程
- 程序:为了完成指定任务,用某种语言编写指令的合集。
- 进程:程序一次执行的过程,或者是正在运行的一个程序。(动态的)进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
- 线程:一个进程中的多个线程共享相同的内存单元/内存地址空间—他们从堆中分配对象,可以访问相同的变量和对象。
使用线程的目的:程序都是从上到下执行的,只有一段程序结束之后,才会执行下一段程序。有时候我们为了提高效率,往往会使用多线程,比如我们下载东西的时候喜欢看斗鱼直播。
比如:
public class Stu {
public void study() {
System.out.println("我正在下载recurdyn");
}
public void watch() {
System.out.println("我想看斗鱼直播");
}
public static void main(String[]args) {
Stu su = new Stu();
for(int i=0;i<5;i++) {
su.study();
}
for(int i=0;i<3;i++) {
su.watch();
}
}
}
//这段代码很简单,输出的一定是:
我正在下载recurdyn
我正在下载recurdyn
我正在下载recurdyn
我正在下载recurdyn
我想看斗鱼直播
我想看斗鱼直播
我想看斗鱼直播
这就是代码执行的顺序性,从上到下依次执行。
要想边下载recurdyn边看腾讯视频,我们就要用到线程,首先来看下线程的创建。
线程的创建的两种方法:
- 继承 Thread
- 实现 Runable
继承 Thread:
通过java.lang.Thread类来实现
- 创建一个继承与Thread()的子类
- 重写Thread里面的run()方法
- 创建Thread的子类的对象
- 通过此对象调用start()方法
//创建一个继承与Thread()的子类
public class StuThread extends Thread{
//重写Thread里面的run()方法
public void run() {
Stu rt = new Stu();
for(int i=0;i<50;i++) {
rt.watch();
}
}
}
public class Stu {
public void study() {
System.out.println("我正在下载recurdyn");
}
public void watch() {
System.out.println("我想看斗鱼直播");
}
public static void main(String[]args) {
//创建Thread的子类的对象
StuThread st = new StuThread();
//通过此对象调用start()方法
//注:这里不能写st.run();如果这样写,线程将不会启动。这样写只是我们创建了一个对象,并调用其中的重写的方法而已。
st.start();
Stu su = new Stu();
for(int i=0;i<500;i++) {
su.study();
}
}
}
//这样的话在输出里面会看到这样一段
我正在下载recurdyn
我正在下载recurdyn
我想看斗鱼直播
我想看斗鱼直播
我想看斗鱼直播
我想看斗鱼直播
我想看斗鱼直播
我正在下载recurdyn
我正在下载recurdyn
我正在下载recurdyn
我正在下载recurdyn
这其实就实现了我们一边看斗鱼直播,一边下载recurdyn
这段代码的执行过程为:
- 先执行主函数里面的代码
- 执行主函数代码期间,Thread的子类的对象的start()方法启动,开始执行线程里面的代码。主线程和分线程之间其实就是谁快谁慢的问题,他们在时间上是同时之执行的。
通过实现Runnable接口
Runnable方式实现多线程 :
- 1.创建实现了Runnable接口的类
- 2.实现Runnable中的抽象方法:run()
- 3.创建实现类的对象
- 4.将此对象传递到Thread类的构造器中,创建Thread类的对象
- 5.通过Thread类的对象来调用start()方法
下面通过代码来演示:
主线程实现遍历1-100的偶数;分线程实现遍历1-100的奇数
public class MyThread {
public static void main(String[] args) {
//3.创建实现类的对象
TThread tt = new TThread();
//4.将此对象传递到Thread类的构造器中,创建Thread类的对象
Thread t = new Thread(tt);
//5.通过Thread类的对象来调用start()方法
t.start();
for(int i=0;i<100;i++) {
if(i % 2 !=0) {
System.out.println("100以内的奇数为:"+Thread.currentThread().getName()+":"+i);
}
}
}
}
//1.创建实现了Runnable接口的类
class TThread implements Runnable{
//2.实现Runnable中的抽象方法:run()
@Override
public void run() {
for(int i=0;i<100;i++) {
if(i % 2 == 0) {
System.out.println("100以内的偶数为:"+Thread.currentThread().getName()+":"+i);
}
}
}
}
/*
输出:(一部分)
00以内的偶数为:Thread-0:0
100以内的奇数为:main:13
100以内的奇数为:main:15
100以内的偶数为:Thread-0:2
100以内的奇数为:main:17
100以内的偶数为:Thread-0:4
100以内的偶数为:Thread-0:6
*/
其实现过程与上图一致;
以上便是常见的创建线程的两种方式。
在这里我们要明确两个问题:
- start();的作用: a.启动线程 b.调用其中的run()方法
- 同一个线程,只能start一次(第一次调用start,threadStatus==0,第二次调用start,threadStatus!=0)
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
。。。。
}
以上两种线程创建方式的对比:
- 类都是单继承多实现的,如果我们使用继承Thread类的方式,会造成不便,而实现Runnable接口的方式不会受此限制。
- 在实现Runnable中其实只需要创建一个类,实现了数据共享,而继承Thread中需要多个类,在数据共享时,需要将共享数据变为静态成员变量。
- 综上所述,实现Runnable接口的方式会更加实用一些。
既然我们已经知道了线程的实现方式及其启动方法,那我们便需要了解一下线程的生命周期。
线程的安全问题
因为之前提到过,多个线程可以对同一个数据进行操作,这就会出现线程的安全问题。要想解决线程的安全问题,我们首先要知道线程的安全问题是怎么出现的。
这就要求我们了解线程的生命周期
在jdk中Thread.State定义了线程的几种状态:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
//Thread t = new Thread(tt);此时start还没有被调用
//创建线程
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
以上是jdk中根据方法分类的,而为了简便,我们自己分类,如下图
- 首先通过继承Thread或者实现Runnable接口创建一个线程
- 通过start方法使得该线程准备就绪,可以运行,但此时不是准备就绪之后便可以运行的,要等CPU,等到该线程获得CPU执行权的时候,该线程才可以执行。
- 获得了CPU执行权,运行其中的run方法。
- run方法执行结束之后,或者是调用stop方法,或者出现出现错误的时候,该线程结束。
- 在运行时,可能会出现线程阻塞,比如sleep,等sleep运行结束之后,会继续从start开始运行该线程。
try{
Thread.sleep(30);
}catch(Exception ef){
ef.printStackTrace();
}
介绍了线程的生命周期之后,就可以聊一聊线程的安全问题了:
这里我以儿子,母亲,父亲取钱为例,写一段代码
比如,我们执行以一段代码:
public class Test2 {
public static void main(String[] args) {
TThread2 t = new TThread2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("儿子");
t2.setName("父亲");
t3.setName("母亲");
t1.start();
t2.start();
t3.start();
}
}
class TThread2 implements Runnable{
private int money = 1000;
public void run() {
while(money > 0) {
System.out.println(Thread.currentThread().getName()+"取完之后,余额为:"+ money);
money--;
}
}
}
/*
其运行结果的一部分为:
儿子取完之后,余额为:1000
母亲取完之后,余额为:1000
母亲取完之后,余额为:998
母亲取完之后,余额为:997
母亲取完之后,余额为:996
母亲取完之后,余额为:995
母亲取完之后,余额为:994
母亲取完之后,余额为:993
母亲取完之后,余额为:992
母亲取完之后,余额为:991
父亲取完之后,余额为:1000
父亲取完之后,余额为:989
父亲取完之后,余额为:988
父亲取完之后,余额为:987
母亲取完之后,余额为:990
儿子取完之后,余额为:999
*/
以上代码操作的是同一个对象,而出现了三次1000,而且钱的余额也不对,这便是出现了线程的安全问题。
接下来,来分析下线程安全问题出现的原因。
线程安全问题出现的原因
我以一个图的形式来展示下:
- 一家三口去取钱,因为取钱这个程序执行需要时间,所以可能存在一种情况,就是儿子再取钱的时候,取钱的程序没有结束的时候,恰好父亲也在取钱,这就会出现余额相同的情况,从而导致线程不安全。
- 解决:加锁,在一个线程执行时,加一个锁来锁住他,不让下一个线程进来,等到前面的线程结束之后,下一个才能进来。
synchronized(同步监视器){
需要被同步的代码
}
其中,同步监视器可以是任何一个类的对象(常用object,this)
对于同步监视器也是有要求的:
- 多个线程要共用同一把锁。
优化之后的代码:
public class Test2 {
public static void main(String[] args) {
TThread2 t = new TThread2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("儿子");
t2.setName("父亲");
t3.setName("母亲");
t1.start();
t2.start();
t3.start();
}
}
class TThread2 implements Runnable{
private int money = 1000;
public void run() {
while(true) {
synchronized(this) {
if(money>0) {
System.out.println(Thread.currentThread().getName()+"取完之后,余额为:"+ money);
money--;
}
}
}
}
}
/*
一部分运行结果:
儿子取完之后,余额为:1000
儿子取完之后,余额为:999
儿子取完之后,余额为:998
儿子取完之后,余额为:997
儿子取完之后,余额为:996
儿子取完之后,余额为:995
儿子取完之后,余额为:994
儿子取完之后,余额为:993
儿子取完之后,余额为:992
儿子取完之后,余额为:991
儿子取完之后,余额为:990
儿子取完之后,余额为:989
儿子取完之后,余额为:988
儿子取完之后,余额为:987
儿子取完之后,余额为:986
儿子取完之后,余额为:985
儿子取完之后,余额为:984
儿子取完之后,余额为:983
儿子取完之后,余额为:982
儿子取完之后,余额为:981
儿子取完之后,余额为:980
儿子取完之后,余额为:979
儿子取完之后,余额为:978
儿子取完之后,余额为:977
儿子取完之后,余额为:976
儿子取完之后,余额为:975
儿子取完之后,余额为:974
儿子取完之后,余额为:973
儿子取完之后,余额为:972
儿子取完之后,余额为:971
儿子取完之后,余额为:970
*/