Java--多线程

本文介绍了Java线程的基础概念,包括线程与进程的区别、线程的创建方式、生命周期及其状态转换等内容。此外,还详细解释了如何使用synchronized关键字进行线程同步,以及如何避免死锁等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程基础

线程,程序运行的基本单元。操作系统中运行的任何程序都至少有一个主线程作为这个程序运行的入口点。

进程与线程

操作系统中可以有多个进程,包括系统进程和用户进程,一个进程可以有一个或多个线程。

进程之间不共享内存,在各自独立的内存空间中运行。一个进程中的线程可以共享系统分派给这个进程的内存空间。

线程不仅可以共享进程内存,还拥有一个属于自己的内存空间--线程栈,在建立线程时由系统分配,主要用来保存线程内部所使用的数据。

操作系统将进程分成多个线程后,这些线程在操作系统的管理下并发执行,提高了程序的运行效率。

线程的执行从宏观上看,是多个线程同时执行,实际上是交替执行的。

线程的意义

1、充分利用CPU资源;

2、简化编程模型

3、简化异步事件的处理

4、使GUI更有效率

5、提高程序的执行效率(增加CPU数、为一个程序启动多个进程、在程序中使用多线程)

Java的线程模型

一个线程类的唯一标准就是这个类是否实现Runnable接口的run()方法--线程执行函数。

Java中建立线程的两种方法:

一是继承Thread类;二是实现Runnable接口,并通过Thread和实现Runnable的类来建立线程。

本质上是一种方法,都是通过Thread类建立线程,并运行run()方法。

区别在于:通过继承Thread类建立线程,虽说实现容易,但Java不支持多继承,继承了Thread类,就不能继承其他类。

通过实现Runnable接口的方法建立线程,这样线程类在必要的时候继承和业务有关的类,而不是Thread类。

创建线程

Java语言中使用类Thread代表线程,所有线程对象都必须是Thread类或其子类的实例。

无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或其子类的实例。

子类中必须要覆盖Thread类的run()方法才能真正运行线程的代码。

public class Thread1 extends Thread{
public void run()
{
System.out.println(this.getName());
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
Thread1 thread1=new Thread1();
Thread1 thread2=new Thread1();
thread1.start();
thread2.start();
}

}


注意:任何一个Java程序都必须有一个主线程,一般主线程名字是main。必须在程序中建立另外的线程,才是真正的多线程程序。

设置线程名:一是利用重载构造方法设置线程名;二是利用Thread类的setName方法修改线程名。

public class Thread2 extends Thread{
private String who;
public void run()
{
System.out.println(who+":"+this.getName());
}
public Thread2(String who)
{
super();
this.who=who;
}
public Thread2(String who,String name)
{
super(name);
this.who=who;
}
public static void main(String[] args) {
Thread2 thread1=new Thread2("thread1","MyThread1");
Thread2 thread2=new Thread2("thread2");
Thread2 thread3=new Thread2("thread3");
thread2.setName("MyThread2");
thread3.start();
thread2.start();
thread1.start();
}

}


使用Runnable接口创建线程

在实现Runnable接口的类时,必须使用类Thread的实例才能创建线程。

使用Runnable接口创建线程的过程分为如下两个步骤:

1、将实现Runnable接口的类实例化;

2、建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法,最后通过Thread类的start()方法建立线程。

public class myRunnable implements Runnable{
public void run()
{
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
myRunnable t1=new myRunnable();
myRunnable t2=new myRunnable();
Thread thread1=new Thread(t1,"MyThread1");
Thread thread2=new Thread(t2);
thread2.setName("MyThread2");
thread1.start();
thread2.start();
}

}


例:使用Thread创建线程

//通过继承Thread类来创建线程类
public class youngThread extends Thread{
private int i;
//重写run()方法,run()方法的方法体就是线程执行体
public void run()
{
for(;i<100;i++)
{
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名
//如果想获取当前线程,直接使用this即可
//Thread对象的getName()返回当前线程的名字
System.out.println(getName()+""+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()+""+i);
    if(i==20) {
    //创建、并启动第一条线程
    new youngThread().start();
    //创建、并启动第二条线程
    new youngThread().start();
    }
    }
}
}

在实例中直接创建的Thread子类即可代表线程对象,在使用Runnable接口创建线程中创建的Runnable对象只能作为线程对象的target。

线程的生命周期

线程需要经历开始(等待)、运行、挂起、停止四个状态。

//开始线程

public void start();

public void run();

//挂起和唤醒线程

public static void sleep(long millis);

public static void sleep(long millis,int nanos);

//终止线程

public void interrupt();

//得到线程状态

public boolean isAlive();

public boolean isInterrupted();

public static boolean isInterrupted();

//join方法

public void join()throws InterruptedException;

创建并运行线程

public class LifeCycle extends Thread{
public void run() {
int n=0;
while((++n)<1000);
}
public static void main(String[] args) throws Exception{
LifeCycle thread1=new LifeCycle();
System.out.println("isAlive:"+thread1.isAlive());
thread1.start();
System.out.println("isAlive:"+thread1.isAlive());
thread1.join();  //等线程thread1结束后再继续运行
System.out.println("thread1已经结束!");
System.out.println("isAlive:"+thread1.isAlive());
}
}

挂起和唤醒线程

线程执行run()方法,一直到执行结束线程才退出。

两种方法使线程暂时停止执行:suspend()和sleep()--被标识为deprecated(抗议)标记,尽量不要使用。

suspend()挂起线程后可以通过resume()唤醒线程。可以在一个线程中挂起另一个线程

sleep()使线程休眠后只能在设定的时间后进入就绪状态,等待系统调度。只对当前正在执行的线程起作用

终止线程

3个方法:

1、使用退出标志,使线程正常退出,也就是run()方法完成后终止;

2、使用stop()方法强行中止线程(不推荐,会发生不可预料的后果);

3、使用interrupt方法终止线程;

第一种:

执行run()方法后,线程会退出。但当使用线程监听客户端请求时或需要做循环处理掉事情时,这个时候一般将任务放到一个循环中,设置一个boolean标志位来终止线程。

public volatile boolean exit=false;

 volatile--使exit同步,也就是说在同一个时刻只能由一个线程来修改exit的值。

第二种:

使用stop()方法--thread.stop(); 不推荐使用,就像突然关闭计算机电源一样,后果不可预测。

第三种:

分两种情况:

1、线程处于阻塞状态,如使用了sleep()方法;

2、使用while(isInterrupted()){...}来判断线程是否被中断;

第一种情况下使用interrupt()方法,sleep()方法将会抛出一个InterruptedException例外;

第二种情况下线程直接退出;

类Thread有两个方法判断线程是否通过interrupt()方法被终止:

一是静态方法interrupted()--判断当前线程是否被中断;

二是非静态方法isInterrupted()--可以判断其他线程是否被中断;

线程阻塞                    


线程死亡

结束后的线程处于死亡状态,3种方式:

1、run()方法执行结束,线程正常结束;

2、线程抛出一个未捕获的Exception或Error;

3、直接调用该线程的stop()方法来结束线程。(该方法容易死锁,不推荐使用)

测试线程是否死亡:调用isAlive(),就绪、运行、阻塞--返回true;新建、死亡--返回false.

对一个已经死亡的线程调用start()方法使它重新启动的想法是很愚蠢的!

public class si extends Thread{
private int i;
//重写run()方法,run()方法中的方法体是线程执行体
public void run() {
for(;i<100;i++) {
//当线程类继承Thread类时,可以直接调用getName方法来返回当前线程名
//如果想获取当前线程,直接使用this即可
//Thread对象的getName返回当前该线程的名字
System.out.println(getName()+""+i);
}
}
public static void main(String[] args) {
//创建线程
si sd=new si();
for(int i=0;i<300;i++) {
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()+""+i);
if(i==20) {
//启动线程
sd.start();
//判断启动后线程的isAlive()值,输出true
System.out.println(sd.isAlive());
}
//只有当线程处于新建、死亡两种状态时isAlive返回false
//因为i>20,则该线程肯定已经启动了,所以只能是死亡状态了
if(i>20&&!sd.isAlive()) {
//试图再次启动该线程
sd.start();
}
}
}
}

控制线程

join()方法:使异步执行的线程变成同步执行。

什么情况下使用join()方法?

使用start()方法启动线程后需要使用这个线程的输出结果。

不使用join()方法的后果?

在执行start后面的语句时,线程不一定执行结束。

volatile关键字:声明简单数据类型。int、float、boolean等

原理:多线程中多个线程操作同一数据时,当有线程更改某一变量时,实际是更改的保存在本地缓存中的数据,在主机内存中的数据并未改变,其他线程不知道变量值被改,使用volatile关键字,将更改的值保存在主机内存中,其他线程就会同步知道。

注意:声明为volatile的简单变量,如果当前值和以前的值相关,那么这个关键字不起作用。

可以使用synchronized关键字:

public static synchronized void inc(){ n++ };

后台线程:前台线程死亡,后台线程也死亡。

睡眠线程:调用sleep()。

线程让步:调用yield()方法--使当前线程暂停,但不阻塞该线程。

实际上,暂停之后,只有优先级与当前线程相同,或者比当前高才能获得执行的机会。

线程传递数据

向线程传递数据的3种方法:

1、通过构造方法传递数据;

2、通过变量和方法传递数据;

3、通过回调函数传递数据;

从线程返回数据的2种方法:

1、通过类变量和方法返回数据;

2、通过回调函数返回数据;

数据同步

数据同步--对于同一个Java类的对象实例,run()方法同时只能被一个线程调用,当前的run()方法执行完后,才能被其他的线程嗲用。

同步--后续操作需要等本次提交的结果才能继续。例如做饭后才能吃饭。

异步--后续操作与本次提交结果没有直接关系。例如一边吃饭一边看电视。

public synchronized void run(){}

静态方法,加上了synchronized关键字,方法就是同步的。

使用synchronized关键字需要注意四点:

1、synchronized关键字不能继承;

2、定义接口方法时不能使用synchronized关键字;

3、构造方法不能使用synchronized关键字,但可以使用synchronized块来同步;

4、可以自由放置synchronized。

synchronized放置位置:

静态方法:可以放在static、void之间,static、public之间,public前;

非静态方法:可以放在void、public之间,public前;

不能放在方法返回类型后面,只能用来同步方法,不能同步类变量。

在类中使用synchronized关键字定义非静态方法,那么将影响这个类中所有使用synchronized关键字定义的非静态方法,定义静态方法一样。

使用synchronized关键字同步类方法

1、非静态类方法的同步

public void method(){

    synchronized(this){...}

}

代码必须全部写在synchronized块中,内部类方法要与外部类方法同步,采用synchronized(外部类名.this){...}。

程序只要退出synchronized块,所持有的同步锁会自动释放。

2、静态方法的同步

调用静态方法时对象实例不一定被创建,所以不能使用this来同步静态方法,必须使用Class对象来同步静态方法。

public void method(){

    synchronized(类名.class){...}

}

在同步静态方法时可以使用累的静态字段class来得到Class对象,也可以使用实例的getClass方法来得到Class对象。

还可以通过Class对象使不同类的静态方法同步。

public class Test{

    public void method(){

        synchronized(Synic.class){...}

    }

}

可以在非静态方法中使用Class对象来同步静态方法,但在静态方法中不能使用this来同步非静态方法。

从类方法的角度来理解,可以通过类变量来同步相应的方法;从类变量的角度来理解,可以使用 synchronized块来保证某个类变量同时只能被一个方法访问。

使用 synchronized块时应注意, synchronized块只能使用对象作为它的参数。如果是简单类型的变量(int、char),不能使用 synchronized来同步。

死锁问题

思索的根源在于不当的使用synchronized关键字。

Java中每个对象都有一把锁与之对应。

常见的死锁及解决办法:

1、数据库死锁

两个或两个以上的连接相互阻塞,则他们都不能继续执行。

解决办法:强制销毁一个连接(通常使用最少的连接),并回滚其事务。出现问题就重试。

2、资源池耗尽死锁

由于负载造成的,即资源池太小,而每个线程需要的资源超过了池中的可用资源。

解决办法:增加了连接池的大小或者重构代码,以便单个线程不需要同时使用很多数据库连接。或者可以设置内部调用使用不同的连接池,即使外部调用的连接池为空,内部调用也能使用自己的连接池继续。

3、单线程、多冲突数据库连接死锁

对同一线程执行嵌套的调用有时出现死锁。

4、Java虚拟机锁与数据库锁冲突

发生在数据库锁与Java虚拟机锁并存的时候。

如何避免死锁?

1、对竞争的资源引入序号;

2、了解数据库锁的发生行为;

3、嵌套调用时,了解哪些调用使用了与其他调用同样的数据库连接;

4、确保在峰值并发时有足够大的资源池;

5、避免执行数据库调用或在占有Java虚拟机锁时,执行其他与Java虚拟机无关的操作;

多线程编程的常见陷阱:

1、在构造函数中启动线程;

例如B继承A,A中的thread先于B启动,并调用了A中的变量,但B的构造函数可能给其赋值,导致两个线程在使用这个变量,没有同步;

解决办法:一是将A设置为final,不可继承;二是提供单独的start()方法用来启动线程,不放在构造函数中。

2、不完全的同步;

当将某个变量在A方法中同步时,则在变量出现的其他地方,也需要同步,除非允许弱可见性甚至产生错误值;

解决办法:通过声明变量为volatile来解决;

3、在使用某个对象当锁时,改变了对象的引用,导致同步失效。

synchronized(array[0]){

...

array[0]=new A();

...}

解决办法:将锁声明为final变量或者引入业务无关的锁对象,保证在同步块内不会被修改引用。

4、没有在循环中调用wait();

5、同步的范围过小或过大;

6、正确使用volatile;

volatile可以用来做什么?

状态标志,模拟控制机制;安全发布,例如修复DLC问题;开销较低的读写锁;

volatile不能用来做什么?

不能用作计数器;与其他变量构成不变式;



























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值