目录
? ? ? ? 进程:在操作系统中每个独立运行的程序就是一个进程,是操作系统进行资源分配和调度的一个独立单位,具有独立性、动态性、并发性。
? ? ? ? 线程:进程的组成部分,是最小的处理单位。
? ? ? ? 多线程与多进程的区别:多线程之间数据块相互独立、互不影响。数据块可以共享。
? ? ? ? 多线程编程的优点:
(1)多线程之间可以共享内存,节约系统资源成本
(2)充分利用CPU,执行并发任务效率高
(3)Java内置多线程功能支持,简化编程模型
(4)GUI应用通过启动单独线程收集用户界面事件,简化异步事件处理
(1)Thread类
? ? ? ? Thread类(线程类),一般用于执行线程的构建、状态的切换,在后续的任务实现中,一般会将实现类继承于Thread,并构建Thread实例化对象,通过start来执行线程,并自动运行run()方法,通过重写run()方法,来完成自己需要实现的任务。
方法 | 功能 |
final boolean isAlive() | 判断线程是否处于激活状态 |
static void sleep(long mills) | 线程休眠,参数以毫秒为单位 |
void run() | 线程的执行方法 |
void start() | 启动线程的方法,启动线程后自动执行run方法 |
void stop() | 线程停止,该方法已过时 |
final String getName() | 获取线程名称 |
final void setName(String name) | 设置线程的名称为name |
long getId() | 获取线程id |
setPriority(int newPriority) | 设置线程的优先级 |
getPriority() | 获取线程的优先级 |
? ? ? ?
(2)Runnable接口
? ? ? ? Runnable接口用于标识某个Java类是否作为线程类,该接口只有一个抽象方法run()。
(3)Callable接口
? ? ? ? Callable接口提供一个call()方法作为线程的执行体,call()方法可以有返回值?,也可以声明抛出异常。
? ? ? ? 由于Callable接口是函数式接口,在Java8之后可以使用Lambda表达式创建Callable对象。
(4)Future接口
? ? ? ? Future接口用来获取Callable接口中的call方法的返回值
? ? ? ? 每个进程至少包含一个线程,即主线程,主线程用来执行main()方法。
? ? ? ? 在main方法中调用Thread类的静态方法currentThread()来获取当前线程。
public class mainthread {
public static void main(String []args)
{
Thread t=Thread.currentThread(); //创建线程对象为当前线程
t.setName("MyThread"); //设置线程名为MyThread
System.out.println(t.getName()); //获取线程名
System.out.println(t.getId()); //获取id
}
}
? ? ? ? 创建线程的方式:继承Thread类、实现Runnable接口、使用Callable和Future接口
? ? ? ? 步骤:
(1)定义一个子类继承Thread类,重写run()方法
(2)创建子类的实例
(3)调用线程对象的start()方法启动该线程。
public class ThreadDemo extends Thread{
public void run()
{
System.out.println("启动run方法"); //重写run方法
}
public static void main(String []args)
{
ThreadDemo td=new ThreadDemo(); //创建Thread对象
td.start(); //线程启动
}
}
????????步骤 :
(1)定义一个类实现Runnable接口
(2)创建一个Thread类的实例,将Runnable接口的实现类所创建的对象作为参数传入Thread类的构造方法中
(3)调用Thread对象的start()方法启动进程
public class ThreadDemo implements Runnable{
public void run()
{
System.out.println("启动run方法");
}
public static void main(String []args)
{
Thread td=new Thread(new ThreadDemo()); //实现Runnable接口的实现类所构建的对象作为参数传入Thread类构造方法中。
td.start();
}
}
? ? ? ? 步骤:
(1)创建Callable接口的实现类,实现call()方法
(2)使用FutureTask类来包装Callable对象
(3)将FutureTask对象作为Thread对象的target。创建并启动新线程
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回
? ? ? ? ?注意:在Callable接口定义中call()的返回值是<T>,也就是说可以任意设计,但不能是void。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadDemo implements Callable{
public String call() //Callable接口实现类,实现call方法
{
return "call启动";
}
public static void main(String []args)
{
FutureTask <String>ft=new FutureTask<String>(new ThreadDemo()); //FutureTask类包装Callable对象
new Thread(ft).start(); //启动新线程
try{
System.out.println(ft.get()); //调用FutureTask类的get方法返回call的返回值。
}
catch(Exception e){
e.printStackTrace();
}
}
}
????????线程生命周期要经过五个状态:新建,就绪,运行,阻塞,死亡。
? ? ? ? 线程状态之间的转换如图所示。
? ? ? ? 当使用new关键字创建一个线程后,该线程就处于新建状态
? ? ? ? 当线程对象调用start()方法后,线程就处于就绪状态,对于每一个线程new创建后,只能调用一次start()方法。
? ? ? ? 多次运行start()方法,会产生异常。
? ? ? ? 处于就绪状态的线程获得CPU后,开始执行run()方法,但下列情况会使线程进入阻塞状态:
(1)调用sleep()方法,主动放弃所占用CPU资源
(2)调用阻塞IO方法,在该方法返回前,线程被阻塞
(3)试图获得一个同步监视器,但该监视器被其他线程所占用
(4)执行条件不满足,调用wait()方法,使线程进入等待状态
(5)程序调用线程的suspend()方法将线程挂起
? ? ? ? 当线程从阻塞状态解除时,会进入就绪状态而不是运行状态,重新等待线程调度。
? ? ? ? 对于sleep()方法:
(1)毫秒为单位
(2)会引发异常,添加try...catch语句
? ? ? ? 使用sleep()方法后,线程进入阻塞状态,输出isAlive()返回false
? ? ? ? 对于isAlive()方法:
(1)若线程处于就绪、运行、阻塞状态时,返回true
(2)若线程处于新建、死亡状态时,返回false
? ? ? ? 结束线程的方式:
(1)线程执行完毕run方法或call方法
(2)抛出一个没有捕获的异常或错误
(3)调用stop()停止线程
? ? ? ? Thread类中的join()方法,用于等待该线程执行完毕,当一个线程调用另一个线程的join()方法时,他会被阻塞,直到被调用的线程执行完毕。在主线程中调用子线程的join()方法,可以确保子线程执行完毕后,再继续执行主线程的后续代码。
? ? ? ? 不要对处于死亡状态的线程调用start()方法,会产生异常。
? ? ? ? 线程的优先级代表线程的重要程度,优先级高的线程获得CPU的机会更多。
? ? ? ? Thread类提供三个静态常量标识线程优先级:
(1)MAX_PRIORITY:最高优先级(值为10)
(2)NORM_PRIORITY:默认优先级(值为5)
(3)MIN_PRIORITY:最低优先级(值为1)
? ? ? ? 另外可以通过setPriority()方法设置线程的优先级,通过getPriority()方法来获取线程的优先级。
? ? ? ? 下面代码示例测试不同优先级的线程的输出先后顺序:
? ? ? ? 一般来说,在重复多次运行情况下,优先级高的线程占据输出的前几行的概率要高一些。?
public class priority extends Thread{
public priority(String name){ //带参构造方法
super(name);
}
public void run(){
for(int i=0;i<10;i++){
System.out.println("线程名:"+this.getName()+", 优先级:"+this.getPriority()+", i="+i);
}
}
public static void main(String[] args)
{
System.out.println("默认线程优先级为:"+Thread.currentThread().getPriority());
priority t1=new priority("t1(最低)");
priority t2=new priority("t2(默认)");
priority t3=new priority("t3(最高)");
t1.setPriority(MIN_PRIORITY); //设置最低优先级
t3.setPriority(MAX_PRIORITY); //设置最高优先级
t1.start();
t2.start();
t3.start();
}
}
? ? ? ? 线程同步保证了某个资源在某一时刻只能由一个线程去访问。
? ? ? ? 线程同步的三种方式:同步代码块、同步方法、同步锁。
? ? ? ? 如果不使用同步,多线程同时访问同一数据就会造成数据丢失修改的错误,产生安全问题。
? ? ? ? 下面代码将通过银行存取钱问题,演示不使用同步时产生的问题:
? ? ? ? 注意:银行存取钱问题一共分为两个类(银行类,存取钱类),一个主函数
(1)建立银行类,变量账户名和账户余额
//银行类
public class Bankaccount {
private String bankNo; //账户名
private double balance; //账户余额
public Bankaccount(String bankNo,double balance)
{
this.bankNo=bankNo;
this.balance=balance;
}
public String getbankNo()
{
return bankNo;
}
public double getbalance()
{
return balance;
}
public void setbankNo(String bankNo)
{
this.bankNo=bankNo;
}
public void setbalance(double balance)
{
this.balance=balance;
}
public String toString()
{
return "账户名:"+bankNo+", 账户余额:"+balance;
}
}
(2)建立非同步情况下的存取钱实现类 ,继承与线程类,并实现run方法进行存取钱操作。
//非同步情况存取钱实现类
public class Nosynbank extends Thread {
private Bankaccount account; //银行账户
private double money; //操作金额
public Nosynbank(String name,Bankaccount account,double money)
{
super(name); //线程名
this.account=account;
this.money=money;
}
public void run()
{
double d=this.account.getbalance(); //获取当前银行账户余额
//money>0代表存钱,money<0代表取钱,当取钱数大于余额时
if(money<0&d<-money)
System.out.println(this.getName()+"操作失败,余额不足");
else
{
d+=money;
System.out.println(this.getName()+"操作成功,当前余额:"+d);
try{
Thread.sleep(1);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
this.account.setbalance(d); //修改当前银行账户余额
}
}
}
(3)测试类,由于多个线程非同步情况下,对于同一个账户的账户金额进行修改,所以四个线程的触发顺序并不按着输入的顺序执行,另外多次重复运行,最后的银行的账户余额也不同。
public class Demo {
public static void main(String[]args)
{
Bankaccount bank1=new Bankaccount("0001", 10000);
Nosynbank t1=new Nosynbank("Thread-1", bank1, 5000);
Nosynbank t2=new Nosynbank("Thread-2", bank1, -2000);
Nosynbank t3=new Nosynbank("Thread-3", bank1, 3000);
Nosynbank t4=new Nosynbank("Thread-4", bank1, 1000);
//启动所有线程
t1.start();
t2.start();
t3.start();
t4.start();
//等待当前所有子线程结束
try{
t1.join();
t2.join();
t3.join();
t4.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println(bank1); //查看当前账户余额
}
}
(1)改写存取钱类,在run方法下添加一行synchronized(this.account),限制run方法的所有代码,其他所有代码均不修改。
public class Synbank extends Thread{
/*
成员变量和成员方法省略
*/
public void run()
{
//同步代码块
synchronized(this.account)
{
double d=this.account.getbalance();
//money>0代表存钱,money<0代表取钱,当取钱数大于余额时
if(money<0 && d<-money)
System.out.println(this.getName()+"操作失败,余额不足");
else
{
d+=money;
System.out.println(this.getName()+"操作成功,当前余额:"+d);
try{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
this.account.setbalance(d);
}
}
}
}
(2)修改测试类,实例化同步存取钱类即可,注意此时的存取钱的线程仍然不按照所编写的顺序去执行,但是最后的银行账户金额修改正确,证明了同步方法避免了丢失修改问题。
public class Demo {
public static void main(String[]args)
{
Bankaccount bank1=new Bankaccount("0001", 10000);
Synbank t1=new Synbank("Thread-1", bank1, 5000);
Synbank t2=new Synbank("Thread-2", bank1, -2000);
Synbank t3=new Synbank("Thread-3", bank1, 3000);
Synbank t4=new Synbank("Thread-4", bank1, 1000);
//启动所有线程
t1.start();
t2.start();
t3.start();
t4.start();
//等待当前所有子线程结束
try{
t1.join();
t2.join();
t3.join();
t4.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println(bank1); //查看当前账户余额
}
}
(1)在银行账户类中添加同步存取钱方法,用synchronized来限制,同时修改银行账户线程的调用方式,不需要设置变量d来存储存取钱后的账户余额。
public class Bankaccount {
/*
成员变量和成员方法的函数省略,与上文相同
*/
//同步方法
public synchronized void access(double money)
{
//money>0代表存钱,money<0代表取钱,当取钱数大于余额时
if(money<0 && balance<-money)
//此时线程名称的表示,由于在类内,没有实例化对象,所以只能通过返回当前线程的名称的方式,代替原有的this.account.getName()
System.out.println(Thread.currentThread().getName()+"操作失败,余额不足");
else
{
//注意同步代码块时另设置了变量d用来存储修改后的银行金额,并通过成员方法进行设置,而在类内不需要这样做。
balance+=money;
System.out.println(Thread.currentThread().getName()+"操作成功,当前余额:"+balance);
try{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
}
?(2)修改同步存取钱类的run方法,调用银行账户类中的access方法。
public class Synbank extends Thread{
/*
省略成员变量和成员方法
*/
public void run()
{
this.account.access(money);
}
}
? ? ? ? 同步锁在同步方法的基础上进行修改。
(1)在银行账户类中,添加ReentrantLock锁对象,注意限制为private final
(2)access方法中对于同步的限制synchronized取消。
(3)在保证线程安全情况之前增加“加锁”操作,就是对access函数内的代码加一个try...finally异常机制,在try...finally之前加锁。
(4)在执行完线程安全代码后进行“释放锁”,加在finally块中,保证无论是否存在异常都会在语句执行完或中断后执行释放锁。
import java.util.concurrent.locks.ReentrantLock;
public class Bankaccount {
/*
其他成员方法和成员变量省略
*/
private final ReentrantLock lock=new ReentrantLock(); //同步锁变量
//同步方法
public void access(double money)
{
lock.lock(); //线程安全情况下加锁,加在try外面
try{
if(money<0 && balance<-money)
System.out.println(Thread.currentThread().getName()+"操作失败,余额不足");
else
{
balance+=money;
System.out.println(Thread.currentThread().getName()+"操作成功,当前余额:"+balance);
try{
Thread.sleep(1000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
finally{ //无论是否加锁,都会执行释放锁
lock.unlock();
}
}
}
? ? ? ? 通过同步代码块、同步方法和同步锁方法都可以达到同步的方式。
? ? ? ? 线程通信可使用Object类中定义的三个方法:
(1)wait():让当前线程等待,并释放对象锁,直到其他线程调用该监视器的notify()或notifyAll()方法来唤醒线程。
(2)notify():唤醒此同步监视器下等待的单个线程,解除该线程的阻塞状态
(3)notifyAll()? :唤醒此同步监视器下等待的所有线程,唤醒次序由系统控制。
? ? ? ? wait和sleep的区别:wait方法调用时会释放对象锁,而sleep方法不会。
??参考书籍:《Java 8 基础应用与开发》QST青软实训编?