文章目录
一、程序、进程、线程
-
程序
是为完成特定任务的一组指令的集合,是静态的 -
进程
是正在执行的程序,是动态的,如QQ,正在播放的音乐App -
线程
是进程的一个实体,如QQ的聊天窗口
每个线程有一套虚拟机栈和程序计数器,每个进程有一套方法区(static变量在方法区)和堆,隶属于同一进程的线程共享堆的方法区和堆。
二、多线程的创建
方式一:继承于Thread类
①创建一个继承于Thread类
的子类
②重写Thead类
的run方法
(将此线程执行的操作声明在run()
中)
③创建Thread类
的子类的对象
④通过此对象调用start()方法
求1-100的所有偶数代码如下:
package ThreadDemo;
//1、创建一个继承于Thread类的子类
class MyThread extends Thread{
//2、重写Thead类的run方法
public void run()
{
for(int i=1;i<=100;i++)
{
if(i%2==0)
System.out.print(i+"\t");
if(i%10==0)
System.out.println();
}
}
}
public class Thread1{
public static void main(String[] args) {
//3、创建Thread类的子类的对象
MyThread thread=new MyThread();
//4、通过此对象调用start()方法
thread.start();
for(int i=1;i<=100;i++)
{
if(i%2!=0)
System.out.print(i+"?");
if(i%10==0)
System.out.println();
}
}
}
我创建了一个线程MyThread
用于输出1-100之间的偶数,在主线程中又编写了一个循环用于输出1-100之间的奇数,并在后面添上?
在调用了thread.start();
以后,线程thread
就开始执行了,同时JVM就去调用thread
中的run()
方法。同时主线程中的循环也开始执行,这两个线程是并发执行,执行哪个线程就看此时CPU的调度策略了
如果不是通过调用start()
方法创建一个新线程后,由JVM去调用run()
方法,而是直接通过创建的对象thread
直接调用run()
方法,那么就只是简单的“创建一个对象,调用对象的方法”,不会有线程并发执行的情况产生。
运行结果:
方式二:实现Runnable接口
①创建一个实现了Runnable接口
的类
②实现Runnable接口
中的抽象方法run()
③创建实现类的对象
④将此对象作为参数传递到Thread类的构造方法
中,创建Thread类的对象
⑤调用Thread的start()方法
package ThreadDemo;
class myThread implements Runnable{
public void run()
{
for(int i=0;i<20;i++)
{
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public class Threada {
public static void main(String[] args) {
myThread t=new myThread();
Thread res=new Thread(t);
res.setName("线程一");
res.start();
}
}
两种方式比较:
优先选择:实现Runnable接口
的方式
原因:
1、没有类单继承的局限性
2、更适合用来处理多个线程共享数据的情况(继承Thread类
的实现方式需要添加static约束
)
联系:public class Thread implements Runnable
,Thread
也实现了Runnable接口
相同点:两种方式都需要重写run()方法
,将需要实现的操作声明在run()方法
中
三、Thread中的常用方法
1、start()
:启动当前线程,调用当前线程的run()方法
2、run()
:通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3、currentThread()
:静态方法,返回执行当前代码的线程
4、getName()
:获得当前线程的名字
5、setName()
:设置当前线程的名字
6、yield()
:释放当前线程的CPU执行权
7、join()
:在线程1中调用线程2的join(),此时线程1就进入阻塞状态,直到线程2执行完毕,线程1才结束阻塞状态
8、sleep(long millitime)
:线程进入睡眠状态
–yield()举例–
yield():释放当前线程的CPU执行权
import static java.lang.Thread.yield;
//创建一个线程,完成1-1000偶数的输出
//遇到i为100的倍数则执行yield释放CPU使用权
class myYieldThread implements Runnable
{
public void run()
{
for(int i=1;i<=1000;i++)
{
if(i%2==0)
System.out.println(Thread.currentThread().getName()+": "+i);
if(i%100==0)
Thread.currentThread().yield(); //当前线程释放 CPU使用权
}
}
}
public class yieldEx {
public static void main(String[] args) {
Thread t1=new Thread(new myYieldThread());
t1.setName("线程1");
t1.start();
for(int i=1;i<500;i++)
{
System.out.println("main: "+i);
}
}
}
在创建的线程myYieldThread
有这么一段条件判断,使得当每次i为100的倍数时,线程myYieldThread
都会释放CPU的使用权
if(i%100==0)
Thread.currentThread().yield(); //当前线程释放 CPU使用权
由于只要i是100的整数时就会释放CPU使用权,然后操作系统就重新分配CPU的使用权
可以看到,到达100的倍数时,线程myYieldThread
释放CPU使用权,而操作系统分配CPU使用权给了主线程,因此主线程main
运行
当主线程main
运行完后,即使线程myYieldThread
到达100的倍数,释放了CPU使用权,由于没有别的线程了(这里只设置了线程myYieldThread
和主线程main
),因此CPU的使用权只能分配给线程1(即线程myYieldThread
的线程名),因此线程1表现出来就是连续执行
需要注意的是,线程1
执行yield()方法
后,虽然释放了CPU使用权,但是还是会与其它线程抢夺CPU使用权(即参与CPU使用权的分配),而不是拱手相让。而是让出CPU使用权,然后所有线程包括释放CPU使用权的线程1
,共同参与CPU使用权的分配,CPU使用权分配到哪个线程,哪个线程就得以执行
下面这个运行结果就是其它线程尚未执行完,并且i为100的倍数,但是线程1
仍然连续执行,就是因为 线程1释放CPU使用权后又恰好得到了操作系统分配的CPU的使用权,因此得以连续执行
四、线程的生命周期
5种状态:
新建
:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪
:处于新建的状态的线程调用start()方法,进入就绪态,等待CPU调度
运行
:就绪态的线程获得CPU调度
阻塞
:被人为挂起或执行输入输出操作时,让出CPU给其他线程,使本线程进入阻塞状态
死亡
:线程完成或发生异常或被强制停止
例题:多窗口卖票
创建3个窗口卖票,总票数为100张
方法1 Thread的子类
Windows类
将其作为卖票的窗口
class Windows extends Thread
{
private int tickets=100;
public void run()
{
while(true)
{
//判断是否有余票
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(100-tickets+1));
tickets--;
}
else
{
break;
}
}
}
}
主函数,开启三个卖票的窗口线程
public static void main(String[] args) {
Windows t1=new Windows();
Windows t2=new Windows();
Windows t3=new Windows();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
截取部分运行结果如下
显然,这样的卖票结果是不合理的,同一个票号只应该出现一次。本意是让三个窗口一起卖,共卖100张票。但现在变成了3个窗口,各卖100张票,显然不符合要求
原因就是在Windows类中的成员变量
private int tickets=100;
我们在创建线程时,用以下方法分别创建了3个对象,而每个对象各自都有一个成员变量tickets
,这就是为什么每个窗口都卖100张票的原因
Windows t1=new Windows();
Windows t2=new Windows();
Windows t3=new Windows();
解决办法就是将这个成员变量tickets
设置为static
属性,这样就是三个对象拥有共同的成员变量,即共享这个静态变量
虽然设置成静态变量解决了三个窗口各卖100张票的问题,但还是存在一个问题,那就是票号重复,此即线程安全问题,这将在后文解决^ ^
方法2 实现Runnable方法
runnableWindows类
class runnableWindows implements Runnable
{
private int tickets=100;
public void run()
{
while(true)
{
//判断是否有余票
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(100-tickets+1));
tickets--;
}
else
break;
}
}
}
主函数
public static void main(String[] args) {
runnableWindows t=new runnableWindows();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
在这里没有像使用继承自Thread创建线程那样有三个窗口各自卖100张票的问题,因为从下面的创建线程的方式可以看出,创建3个线程都是使用同一个runnableWindows对象
,因此这3个线程共享同一个tickets
变量,因此在这里不用加上static修饰
runnableWindows t=new runnableWindows();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
但是仍然存在票号重复的线程不安全问题,这个问题也将在后文解决^ ^
五、线程同步
由窗口卖票例题可以看出,可能会重票(有两张及两张以上相同票号的票)或者错票(票号不合理,如100张票,出现了票号为101的情况)的问题
出现这些问题的原因是如下代码造成的线程不安全
public void run()
{
while(true)
{
//判断是否有余票
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
tickets--;
}
else
break;
}
}
重票:假设此时有100张票
此时tickets=100
线程1
:tickets>0
—售票,票号为1—由于某些原因线程1
阻塞,未能执行tickets自减
操作
由于线程1
尚未执行成功tickets自减
操作,因此tickets=100
此时线程2
获得CPU使用权,线程2
执行
线程2
:tickets>0
—售票,票号为1—…
因此出现了重票
错票:假设此时还剩余1张票
此时tickets=1
线程1
:tickets>0
—售票,票号为100—由于某些原因线程1
阻塞,未能执行tickets自减
操作
由于线程1
尚未执行成功tickets自减
操作,因此tickets=1
此时线程2
获得CPU使用权,线程2
执行
线程2
:tickets>0
—线程2
进入到if执行语句块里,但是尚未执行语句,由于某些原因线程2
阻塞
此时线程1
解除阻塞,tickets自减
,tickets=0
,但是由于线程2
已经进入到if执行语句块里,因此当线程2
继续执行时,无需再判断条件是否合理,于是线程2
执行if执行语句块,此时售票,票号为101。只有100张票,票号应为1-100,因此出现了错票
线程安全问题出现的原因:某个线程在操作票的过程中,尚未完成操作时,就有其它线程参与进来,也操作票
解决方法:当一个线程a
在操作tickets
(共享数据)时,其它线程不能参与进来,直到线程a
操作完tickets
,其它线程才可以开始操作tickets
具体方法:通过同步机制来解决线程安全问题
方式一:同步代码块
synchronized(同步监视器)
{
//需要被同步的代码
}
说明:
1、操作共享数据的代码,即为需要被同步的代码
2、共享数据:多个线程共同操作的变量,比如例子中的票数tickets
3、同步监视器,俗称为锁。任何一个类的对象都可以充当锁
要求:多个线程必须共用同一把锁
1 解决实现Runnable方法的方式的线程安全问题
在售票例子中需要被同步的代码块是:
那么为其代码块实现同步如下
这里解决的是实现Runnable的线程安全问题,解决继承Thread类的线程安全问题不能这么做。因为多个线程共用同一个同步监视器来实现同步机制,而继承Thread类创建线程不满足此条件,因此不能使用上述方法解决其线程安全问题
实现Runnable创建线程
runnableWindows t=new runnableWindows();
//创建线程,使用的参数是同一个对象t
//因此只会进行一次Object obj=new Object();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
继承Thread类创建线程
class Windows extends Thread
{...}
//创建线程,3个线程各进行一次Object obj=new Object();
Windows t1=new Windows();
Windows t2=new Windows();
Windows t3=new Windows();
一定要注意看是否用的是同一把锁
锁是任何一个类的对象,可以是当前调用的对象,使用this
也是可以的
使用同步机制的优点和局限性:
优点:解决了线程的安全问题
局限性:操作同步代码块时,只能有一个线程参与,其余线程需要等待,相当于是一个单线程的过程,降低了效率
2 解决继承Thread类的方式的线程安全问题
同样地,线程安全问题出现在
那么需要考虑同步监视器应该如何设置
像解决Runnable方式的线程安全问题那样
Object obj=new Object(); //错误
这不适用于继承Thread方式,因为在创建多个线程时,每个线程都将会创建一个obj,那么就不是多线程共用同一个同步监视器了。我们可以考虑将其使用static
修饰,这样就可以多线程共用同一个同步监视器了
在实现Runnable接口创建多线程的方式中,可以考虑使用this作为同步监视器
synchronized(this)
{...}
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用当前类同步监视器
synchronized(Windows.class)
{...}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,那么我们就可以将这个方法声明成同步的
使用同步方法解决实现Runnable接口的线程安全问题
原来的代码块
class runnableWindows implements Runnable
{
private int tickets=100;
public void run()
{
while(true)
{
//判断是否有余票
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
tickets--;
}
else
break;
}
}
}
既然要使用同步方法解决,那么就是在方法定义的时候加上关键字synchronized
,那么就需要一个方法,因此把售票的过程封装成一个方法sellTickets()
1 使用同步方法解决实现Runnable接口的线程安全问题
class runnableWindows implements Runnable
{
private int tickets=100;
public void run()
{
while(true)
{
sellTickets();
}
}
public synchronized void sellTickets() //默认同步监视器是this
{
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
tickets--;
}
}
}
2 使用同步方法解决继承Thread类的线程安全问题
原来的代码块
class Windows extends Thread
{
private static int tickets=100;
public void run()
{
while(true)
{
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + (100 - tickets + 1));
tickets--;
} else {
break;
}
}
}
}
使用同步方法解决继承Thread类的线程安全问题
class Windows extends Thread
{
private static int tickets=100;
public void run()
{
while(true)
{
sellTickets();
}
}
public static synchronized void sellTickets() //同步监视器就是当前的类:Windows.class
{
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + (100 - tickets + 1));
tickets--;
}
}
}
和Runnable
的解决方法不同之处在于,Thread
的同步方法还多了static
限定
总结:
- 同步方法仍然涉及到同步监视器
- 非静态的同步方法,同步监视器为:this,即当前对象
- 静态的同步方法,同步监视器为: 当前类本身