目录
1.创建多线程的第一种方式:继承Therad,重写Thread的run方法,调用start方法启动
6.线程的生命周期
7.多线程安全问题解决办法-----synchronized:
2.使用继承Thread方式,要注意共享的变量设置成静态,这样才是唯一的,每个窗口都是共享这100张票
创建多线程的第一种方式:继承Therad,重写Thread的run方法,调用start方法启动
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
//主线程的
MyThread myThread = new MyThread();
//还是主线程帮你调用,使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
//如果调用run()那么还是在主线程上执行
myThread.start();
//如果要在执行一次,那么就得在新建对象执行,不能重复执行start(),不然会报错
//MyThread myThread1 = new MyThread();
//myThread1.start();
//主线程的
for (int i = 0; i < 100; i++) {
System.out.println(i + "main");
}
}
}
结果:
0main
1main
2main
3main
4main
......
100main
也可以不使用继承,创建Thread类的匿名子类的方式
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getPriority() + "=" + i);
if (i == 20) {
yield();
}
}
}
}.start();
new Thread() {
@Override
public void run() {
setPriority(10);
for (int i = 0; i < 100; i++) {
System.out.println(getPriority() + "=" + i);
}
}
}.start();
}
上面这个结果:你以为第一个线程到20的时候会让另一个线程运行,实际上不一定,以下是我测试的某次结果,会发现到20都没开始避让。
5=19
5=20
5=21
5=22
10=0
10=1
10=2
Thread常用方法
- start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法
- run():要在新线程种执行的方法
- currentThread():静态方法,返回对当前正在执行的线程对象的引用
- getName():获取当前线程名字
- setName():设置当前线程名字
- yield():暂停当前正在执行的线程对象,并执行其他线程
- join():阻塞当前调用它的线程,等待join执行完毕,当前线程继续执行
- sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),阻塞状态
- setPriority(int newPriority):更改线程的优先级,线程的优先级用1~10 表示,1的优先级最低,10的优先级最高,默认值是5;高优先级抢占低优先级CPU执行权,只是从概率上来讲,比较高,不一定成功
个别方法问题:
1.面试官:请问启动线程是start()还是run()方法,能谈谈吗?
应聘者:start()方法
当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体(真正的逻辑)。
2.在上面那个例子,关于yield方法,参考:https://www.cnblogs.com/java-spring/p/8309931.html
Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,
有可能是其他人先上车了,也有可能是Yield先上车了。
但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,
最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。
3.join方法案例,参考https://blog.csdn.net/u013425438/article/details/80205693?depth_1-
public class JoinTetst {
public static void main(String[] args) throws InterruptedException {
ThreadTest a = new ThreadTest();
ThreadTest b = new ThreadTest();
System.out.println(Thread.currentThread().getThreadGroup().activeCount());
System.out.println("主线程开始");
System.out.println("a开始");
a.setName("线程a");
a.start();
System.out.println(a);
a.join();
System.out.println("a结束");
b.setName("线程b");
b.start();
System.out.println("主线程");
}
}
class ThreadTest extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
}
}
}
2
主线程开始
a开始
Thread[线程a,5,main]
线程a-1
线程a-2
线程a-3
线程a-4
线程a-5
a结束
主线程
线程b-1
线程b-2
线程b-3
线程b-4
线程b-5
可以看出,t1开始和结束的语句打印,都是在A线程执行前后,说明是把调用join()的当前线程挂起,让t1线程执行完毕之后,再开始主线程继续往下执行。
join源码,哪个线程调用的wait()呢,其实哪个线程调用的join就是那个线程,所以main线程会被挂起。
t1调用的wait,是指t1对象调用的wait,而不是t1线程调用的wait,这个地方是相当于main线程调用了t1对象的wait方法,不要把把t1对象和t1线程搞混了。
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
创建多线程的方式二:实现Runnable接口
1.创建一个实现了Runnable接口的类
2. 实现类去实现Runnable中的抽象方法: run()
3. 创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
public class Test1 {
public static void main(String[] args) {
rThead r = new rThead();
Thread t1 = new Thread(r);
t1.start();
//还要在执行一次,在新建就好
Thread t2 = new Thread(r);
t2.start();
}
}
class rThead implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "=" + i);
}
}
}
比较创建线程的两种方式。
开发中:优先选择:实现Runnable接 口的方式,原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
联系: public class Thread impl ements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
线程的生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
-
新建:就是刚使用new方法,new出来的线程;
-
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
-
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
-
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
-
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
多线程安全问题解决办法-----synchronized:
模拟火车站售票程序,有100张票,开启三个窗口售票,买票前先打印剩余票数,然后在减少票数。
1.我们使用实现Runnable来举例
public class Sell {
public static void main(String[] args) {
window window = new window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.start();
thread2.start();
thread3.start();
}
}
class window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket);
ticket--;
}
}
}
结果: 打印哪个线程查询剩余票数,然后票数减1
Thread-0=100
....
Thread-2=61
//票数相同
Thread-0=60
Thread-1=60
Thread-2=59
......
Thread-1=5
Thread-0=4
Thread-2=3
Thread-1=2
Thread-0=1
//出现0,-1的票数
Thread-2=0
Thread-1=-1
会发现,还剩-1和0的票数竟然买了,明显不正确,为什么呢?
我们看最后三条打印记录
阻塞:
- 因为我们使用了sleep方法,当Thread-0进来被休眠变成阻塞状态
- 这时cpu把资源就给了另一个线程Thread-2执行,进到run方法,也被阻塞
- cpu又把资源给了Thread-1,进到run方法,也被阻塞
苏醒(休眠完毕):
- 这时Thread-0首先休眠完毕,发现还有1张票,就打印出来,然后减1等于0
- 随后Thread-2第二个醒来,发现票数变为0就打印,再减1等于-1
- Thread-1也醒来,发现票数变为-1就打印,再减1
其实期间,还会出现票数相等的状况,为什么呢?
- 因为两个线程按顺序苏醒了(相差非常小的时间)
- Thread-0执行打印语句 Thread-0=60,本该按顺序在执行ticket--
- 但是这时CPU却切换到另一个线程Thread-1,导致Thread-0打印出来却还没票数减1
- 所以Thread-1线程开始执行打印语句,也打印出 Thread-1=60 票数相同
解决办法:
当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,b线程才可以开始操作ticket。这种情况即使线程a出现阻塞,也不能被改变。
方式一:同步代码块,只能有一人操作,相当于单线程,效率低
synchronized(同步监视器(锁)){
//需要被同步的代码}
说明:
- 操作共享数据的代码,即为需要被同步的代码
- 共享数据:多个线程共同操作的变量。比如: ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要共用同一把锁,就是这个对象必须是唯一的
我们只需修改,Window类
1.使用锁对象
class window implements Runnable {
private int ticket = 100;
Object object = new Object();
@Override
public void run() {
while (ticket > 0) {
synchronized (object) {
//进来还得在判断一次,否则可能其他线程,已经进到while循环里,不判断的话就会继续减,出现1,-0的状况
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
}
}
2.使用this,因为this指向当前对象,且当前对象只有一个,被new了一次,所以每个线程是使用同一个锁
class window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
//进来还得在判断一次,否则可能其他线程,已经进到while循环里,不判断的话就会继续减,出现1,-0的状况
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
}
}
执行结果,就是按顺序100到1打印了,注意,synchronized (object)不能包含到while (ticket > 0),否则就是一直同一个线程(单个窗口)在执行语句(卖票),一直等while循环结束才放弃锁
哪个线程抢到锁,谁就能运行,不管他阻不阻塞,若果sleep参数是300,那么就是每三秒才卖出一张票(每三秒打印出语句)。
2.使用继承Thread方式,要注意共享的变量设置成静态,这样才是唯一的,每个窗口都是共享这100张票
1.锁对象要设置成静态
public class Sell {
public static void main(String[] args) {
window w1 = new window();
window w2 = new window();
window w3 = new window();
w1.start();
w2.start();
w3.start();
}
}
class window extends Thread {
//要设置成静态否则,每次new对象都是新的一个ticket,不共享
private static int ticket = 100;
//锁也是要设置成静态,不然每次new对象都是新的一个objcet,锁就不唯一了
private static Object object = new Object();
@Override
public void run() {
while (ticket > 0) {
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
}
}
2.锁使用当前对象的class,就不用再new一个静态锁对象了,window 改成以下这样
class window extends Thread {
//要设置成静态否则,每次new对象都是新的一个ticket,不共享
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
synchronized (window.class) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
}
}
方式二:同步方法
1.实现Runnable,把操作共享数据的代码写成一个方法,使用synchronized,不能在run使用synchronized,因为会把while包起来,相当于,一个窗口(线程)一直循环卖票,就不能三个窗口一起卖了。
public class Sell {
public static void main(String[] args) {
window window = new window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.start();
thread2.start();
thread3.start();
}
}
class window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
buy();
}
}
public synchronized void buy() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
2.继承Thread,synchronized使用方式
public class Sell {
public static void main(String[] args) {
window w1 = new window();
window w2 = new window();
window w3 = new window();
w1.start();
w2.start();
w3.start();
}
}
class window extends Thread {
//要设置成静态否则,每次new对象都是新的一个ticket,不共享
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
buy();
}
}
public synchronized void buy() {//同步监视器(锁),w1,w2,w3
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
}
如果也像Runnable那样处理,结果会发现还是出现问题,为啥呢?
因为每次都是new一个window对象,同步代码块synchronized 的锁是本身,每个人都有自己的锁,肯定不行,所以要设置成静态方法,共享一个方法。
......
Thread-0=3
Thread-1=2
Thread-0=1
Thread-2=0
Thread-1=-1
改成如下,加个static就行,这时的锁对象就是window.class,那就都是同一个了。
public static synchronized void buy() {//同步监视器window.class
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + ticket--);
}
}
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是: this
- 静态的同步方法,同步监视器是:当前类本身,即类.class