本节内容
-
1.线程&进程&主线程&子线程概念
-
2.使用继承Thread开启线程
-
3.使用Runnable接口开启线程
-
4.线程的生命周期
-
5.线程的终止
-
6.线程礼让yield()
-
7.多线程导致的问题
-
8.使用lock和synchronized
-
9.线程间通信
一、线程&进程&主线程&子线程概念
1.进程:正在运行/执行的一个程序,进程是用于管理所有的资源的,不进行实际任务
2.线程:完成具体任务(一个进程里面可以有多个线程)。例如QQ运行起来,可以和一个人聊天,又可以和另一个人视频,同时还可以刷空间,这些任务都可以同时进行,这都是由线程来操作的。
3.程序:静态的代码,如果不运行起来就没有意义。而一个程序运行起来就是一个进程,其中又包含多种线程。
4.主线程:在Java中,main方法里面的代码就在主线程中跑。在Android和IOS中,主线程就是启动程序,开到的UI界面,也叫UI主线程
5.子线程:除了主线程之外的都是子线程
6.为什么使用多线程:在主线程里面,任务的执行顺序是从上至下的。如果其中一个任务需要花费大量时间(下载数据等),那么这个任务后面的任务就会被阻塞,必须等这个任务结束才能被执行。这样用户的体验效果不好,这个时候就需要将耗时的任务放在另外一个子线程里面执行。这个就叫多线程。
7.注:不管是主线程还是子线程,都有自己独立的内存空间(执行路径/生命周期)。子线程也要从上至下执行。一个线程只完成一个任务,但是一个进程里面有多个线程。
二、使用继承Thread开启线程
1.获取当前线程信息,调用Thread.currentThread()即可
System.out.println(Thread.currentThread());
2.如何开启线程:①写一个类继承于Thread ②写一个类实现Runnable接口
3.使用继承Thread开启线程的具体过程:首先创建类继承于Thread,具体执行的任务放在run方法里面
class TestThread extends Thread {
public TestThread(String s) {
super(s);
}
@Override
public void run() {
//线程需要执行的任务
for (int i = 0; i < 100; i++) {
System.out.println((i+1)+ " ");
}
}
}
接着在main函数里面创建具体对象,然后就可以通过start()启动线程了。不能调用run方法,不然它还在主线程里面。调用start方法是因为,系统会自动将这个任务放到队列中,等待调度
TestThread t =new TestThread("子线程");
t.start();
注:可以给我们自己创建的类添加一个构造方法,然后创建对象的时候可以自己给这个线程命名。
4.可以创建多个对象,然后调用start()方法。但是没有一定的先后顺序。线程是通过抢占时间片来获取运行机会的,谁抢到时间片,谁就可以运行。时间片是由操作系统来分配的。所以每一次执行结果大部分是不一致的。
三、使用runnabld接口开启线程
1.创建一个类,实现runnable接口(这个类只是一个任务,并不能创建线程)
class TestRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for (int i=1;i<=100;i++){
System.out.println(i);
}
}
}
2.创建一个自定义类的一个具体对象,然后创建一个Thread对象(它可以创建线程),让这个线程去执行testRunnable的任务,线程名字为子线程。然后启动线程。
TestRunnable t= new TestRunnable();
Thread t1= new Thread(t,"子线程1");
t1.start();
注:只创建一个TestRunnable类的对象无法开启子线程,必须依赖于Thread类
3.如果想要两个线程,那么可以再定义一个Thread类
TestRunnable t= new TestRunnable();
Thread t1= new Thread(t,"子线程1");
Thread t2 =new Thread(t,"子线程2");
t1.start();
t2.start();
注:当调用start方法时,这个线程会自动扔到操作系统的任务队列(线程池)中,至于这个任务什么时候被执行,我们无法确定,由操作系统来决定
两种启动线程的方式对比
方式①:使用继承Thread ,方式②:使用runnabld接口
1.创建对象的过程方式①要简便一点,方式②要复杂一点
2.方式①不能再继承于其他的类了,不能实现其他的功能,因为Java是单继承。但是方式②可以继承多个接口,还能实现更多的功能。
3.综上:方式②灵活一点,更易于扩展。所以最好用接口的方式来开启线程
四、线程的生命周期
1.线程的生命周期:从创建到结束
2.线程的五个状态:创建状态—>就绪状态—>运行状态—>死亡状态,运行状态受阻时可能出现阻塞状态。
比如在运行状态调用sleep()或join()就会进入其他阻塞(等休眠时间到或者join()线程执行完毕,IO流阻塞结束,然后就会返回就绪状态),调用wait()就会进入等待阻塞(当使用notifiy()唤醒的时候就会回到就绪状态),调用synchronized()就会进入同步阻塞(当这个锁解开了,就会进入就绪状态)
注:就绪状态得到时间片就变成运行状态,运行状态失去时间片就变回就绪状态。运行状态之后,如果run方法结束,那么就进入死亡状态。
①创建状态(NEW):new Thread()
②就绪状态:1.新的线程调用start(),2.阻塞条件结束,3.正在运行的线程时间片被其他线程抢夺
③运行状态(RUNNABLE):从就绪状态到运行状态是由操作系统实现的,外部无法干预
④死亡状态:1.run方法结束 2.手动让线程暂停 (比如调用stop),但是不建议使用stop,而建议通过其他方式让线程暂停。
⑤阻塞状态(BLOCKED):运行状态受阻时进入阻塞状态
注意:可以调用getState()方法来查看线程当前的运行状态,在创建对象前后调用显示的结果不一样
System.out.println(t1.getState());
如果想要进入阻塞状态,让它显示TIMED_WAITING,那么可以在自己定义的类里面添加一个sleep()方法,再调用getState()方法就可以显示这个,在网络延时的时候可能会使用sleep()
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
五、线程的终止
1.如何让一个线程结束:①不要直接调用stop()方法来结束一个线程 ②自己写一个变量/标识符,用来标识线程结束的临界点
首先在自己写的类里面定义一个变量
private boolean shouldStop = true;
然后再创建一个方法,里面就是对shouldStop做一个赋值。并且在run方法里使用while循环,让它做参数
public void terminated(){
shouldStop=false;
}
public void run() {
while (shouldStop){
System.out.println("子线程");
}
然后在主类里面调用一下这个方法,就可以自己暂停。因为主类会不断的抢占时间片,所以有可能一直输出不了子线程
MyThread t= new MyThread("测试线程");
t.start();
for(int i=0;i<100;i++){
System.out.println("主线程"+(i));
if(i==2){
t.terminated();
}
}
六、线程礼让yield()
1.调用yield()函数可以让其他线程先执行,礼让的线程就会直接进入就绪状态,如果这个线程再次获得时间片就会执行,可能礼让失败
TestRunnable t= new TestRunnable();
Thread t1= new Thread(t,"小美");
t1.start();
for (int i=0;i<20;i++){
System.out.println("主线程"+i);
if(i==5){
Thread.yield();
}
}
以上就是当i=5的时候,for循环就会先让t1这个线程执行,礼让给它之后,它进入就绪状态,然后和t1线程一起争夺时间片。因为它就在主线程里面,所以如果时间片争夺成功,就又会进入运行状态。这样就可能导致礼让不成功。
2.也可以调用join()方法,让其他线程插队,那么当前线程就会进入阻塞状态。只有当插队的线程执行完毕,当前线程才会进入运行状态。
TestRunnable t= new TestRunnable();
Thread t1= new Thread(t,"小美");
t1.start();
for (int i=0;i<20;i++){
if(i==5){
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程"+i);
}
七、多线程导致的问题
1.多线程的优点:执行效率高,不会阻塞主线程(因为耗时的任务都在子线程里面)
2.多线程的缺点:如果多个线程操作同一个资源,有可能出现不安全
public class MyClass {
public static void main(String[] args) {
TicketWindow t1 =new TicketWindow("重庆");
TicketWindow t2= new TicketWindow("成都");
t1.start();
t2.start();
}
}
class TicketWindow extends Thread {
public TicketWindow(String s) {
super(s);
}
private static int total = 100;
@Override
public void run() {
for (int i=0;i<20;i++){
if (total > 0) {
System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
total--;
}
}
}
例如实现以上代码,结果显示为以下,他们第一张买到了一样的票
当前出票口为:重庆 票号为:20
当前出票口为:成都 票号为:20
当前出票口为:成都 票号为:18
当前出票口为:成都 票号为:17
当前出票口为:成都 票号为:16
当前出票口为:成都 票号为:15
···
八、使用lock和synchronized
两种方式保证线程安全:①锁 Lock ②线程同步
1.锁:必须使用的是同一个锁,先创建一把锁
private static Lock lock= new ReentrantLock();
然后调用lock上锁,再调用unlock解锁,把它们放在实现功能的代码前面与末尾即可,以上面买票举例
lock.lock();
if (total > 0) {
System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
total--;
}
//解锁
lock.unlock();
这样就不会买到一样的票了
2.线程同步synchronized,必须保证锁的是同一个对象,每一个对象都维护一把锁,可以修饰代码块,也可以修饰方法
(1)修饰代码块:使用时可以先创建一个Object类对象,然后将这个对象作为synchronized方法的参数,然后将要锁的代码用synchronized包裹起来。以下代码前后并不连续
private static Object object= new Object();
synchronized (object) {
if (total > 0) {
System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
total--;
}
}
(2)修饰方法:在定义函数时添加一个synchronized,随时都可以调用
private synchronized void BuyTickets(){
if (total > 0) {
System.out.println("当前出票口为:" + getName() + " 票号为:" + total);
total--;
}
}
3.synchronized的好处:当第一个线程进这个锁的时候,会执行相应的代码,当第二个线程过来的时候不能立刻执行这段代码,因为它被锁住了。只有等第一个线程解锁以后,第二个线程才会开始
注:不管是锁代码块还是锁方法,尽量让锁的范围小一点。范围太大,其他线程等的时间就越长,这样效率反而会下降。
九、线程间的通信
1.线程间的通信也就是交互,有三种方法可以实现线程间的交互:wait():让某个线程等待, notify():唤醒某个线程, notifyAll():唤醒多个线程
2.注意:这三个方法必须由同步监视器来调用,也就是大家都在抢同一个资源(或者说用同一个锁锁定的资源对象)
3.例如:我们要让两个线程分别交替输出,第一个线程输出以后,调用wait方法,等它输出结束后再调用notify()方法将其它在这同一个锁里等待的线程唤醒
4.如果某个创建后只会使用这一次,那么可以使用匿名内部类的方法,甚至连task这个对象都不用创建,直接调用start方法
public class MyClass {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(i+1);
}
}
}).start();
}
}
5.锁接收一样的参数,可以保证是同一个锁,线程在争夺同一个资源
6.让两个线程交替输出的实战:比如让数字和英文字母交替输出
public class MyClass {
static int state=1;
static Object obj=new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
int num=1;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
while (true){
synchronized (obj){
//判断当前是不是在输出字母
if(state!=1){
//当前这个线程需要等待一下
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出数字
System.out.println(num);
num++;
if(num>26){
break;
}
//唤醒当前obj锁上的其他等待的线程
state=2;
obj.notify();
}
}
}
}
}).start();
new Thread(new Runnable() {
char alpha='a';
@Override
public void run() {
while (true){
synchronized (obj){
//判断当前是不是在输出数字
if(state!=2){
//当前这个线程需要等待一下
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Character.toChars(alpha));
alpha++;
if(alpha>'z'){
break;
}
state=1;
obj.notify();
}
}
}
}).start();
}
}
7.让数字和英文字母交替输出还有另一种方法,我们可以创建一个类,然后在这个类里面添加两种方法,分别是输出数字和输出字母,并且给这两种方法加锁。然后创建一个新类的对象,通过这个对象调用相应的方法
public class MyClass {
static Data d= new Data();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
d.printNum();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
d.printAlpha();
}
}).start();
}
public static void test(){
}
}
class Data{
int num =1;
int alpha='a';
int state=1;
public synchronized void printNum() {
while (true){
if (state!=1){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(num);
num++;
if(num>26){
break;
}
state=2;
this.notify();
}
}
public synchronized void printAlpha(){
while (true){
if(state!=2){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Character.toChars(alpha));
alpha++;
if(alpha>'z'){
break;
}
state=1;
this.notify();
}
}
}
OK,以上就是今天的全部内容,再见!
网友评论