一、线程的调度与控制
通常计算机只有一个CPU,CPU在某一个时刻只能执行一条命令,线程只有得到CPU时间片,也就是使用权,才可以执行命令。在单核CPU的机器上,线程并不是并行运行的。java虚拟机主要负责线程调度,取得CPU的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java使用抢占式调度模型。
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型:优先级高的线程获取CPU时间片相对会多一些,如果线程的优先级相同,那么会随机选用一个 。
下面我们将通过代码来了解如何在java代码中使用线程,一般我们在工作中是不会去写多线程的代码,但如果你是写服务器端代码,如在IBM公司或者写机器之间的通信,自己写一个服务器端的代码,那我们就必须考虑多线程了,正常我们用的服务器像tomcat等已经底层实现多线程了。
1、关于线程的一些基本方法
(1)获取当前线程对象 Thread.currentThread();
(2)给线程起名 t.setName("t1");
(3)获取线程的名字 t.getName();
public class _04线程调度 { public static void main(String[] args){ //如何获取当前线程对象? //t保存的内存地址指向的线程是“主线程对象” Thread t=Thread.currentThread(); //获取线程的名字 System.out.println(t.getName()); Thread t1=new Thread(new Processor03()); //给线程起名 t1.setName("t1"); t1.start(); Thread t2=new Thread(new Processor03()); //给线程起名 t2.setName("t2"); t2.start(); } } class Processor03 implements Runnable{ public void run(){ //t保存的内存地址指向的线程是“t1线程对象” Thread t=Thread.currentThread(); System.out.println(t.getName()); } }
2、线程的优先级
线程优先级高的线程获取的cpu时间片相对多一些,也就是获得执行的机会会大一些,优先级 1-10
线程的优先级主要分三种:MAX_PRIORITY(最高级)(10);MIN_PRIORITY(最低级)(1);NOM_PRIORITY(标准)默认(5)
获取以及设置线程的优先级方法为Thread.getPriority()和Thread.setPriority(),
从运行结果中可以看到t1线程和t2线程抢夺cpu时间片,由于设置了t1的优先级高一些,所以t1抢夺的时间片会相对多一些,所以执行的会多一些
3、Thrad.sleep(ms),线程休眠的方法,关于线程休眠有如下几点注意:
1.Thread.sleep;
2.sleep方法是一个静态方法
3.该方法的作用,阻塞当前线程(回顾一中线程的执行流),腾出CPU让给其他线程
4.此方法是静态方法,所以"引用."和"类名."是一样的
下面的代码中,t.sleep等同于Thread.sleep,故阻塞的并不是t线程,而是阻塞的主线程。
public class _07面试题 { public static void main(String[] args) throws Exception{ Thread t=new Processor07(); t.setName("t"); t.start(); //等同于Threadsleep //Thread.sleep()是静态方法,所以t.sleep()等同于Thread.sleep() //所以t线程是不会阻塞的,阻塞的是主线程 t.sleep(5000); } } class Processor07 extends Thread{ public void run(){ for(int i=0;i<3000;i++){ System.out.println(Thread.currentThread().getName()+"---->"+i); } } }
5.中断休眠的方法
(1)使用异常机制 t.interrupt(),此interrupt会触发Interrupted异常可以从休眠中退出。
(2)正常终止,正确的停止一个线程,在外面增加一个表示位
二、线程的同步(加锁)
1、异步编程模型与同步编程模型。
异步编程模型:t1线程执行t1的,t2线程执行t2的。两个线程谁也不等谁
同步编程模型:t1线程和t2线程执行,当t1线程必须等t2线程执行结束之后,t1线程才能执行,这是同步编程模型
2、什么时候同步呢?为什么要引入线程同步呢?
1.为了数据安全。暂时就不考虑效率了,尽管应用程序的使用率降低,但是为了保证数据是安全的必须加入同步机制,线程是同步的,所以程序就变成了单线程,就像两条直线
原来是平行,现在要接在一起变成一根线。
比如现实生活中,我和你共享一个银行账户,此时这个账户里有5万块钱,今天我从这个账户里取了两万块钱,刚取出来,在账户还没有减掉2万块钱的时候(此时账户里还是5万,但我已经取了2万),
你也来访问这个账户取了3万块,这时你返回给账户是5万减3万,账户还有2万,这是取钱的反馈信息也返回,账户还剩3万。最后结果是,账户原本有5万,我取了2万,你取了3万,账户里还有3万,这
样就出现的账户安全问题,下面我们会用这个例子来做说明。
2.什么条件下要使用线程同步?
(1):必须是多线程环境
(2):多线程环境共享同一个数据
(3):共享的数据涉及到修改操作
以下程序演示取款例子,首先我们看一下不使用线程同步机制,多线程同时对同一个账户进行取款操作,会出现什么问题?
首先我们创建一个Account类模拟账户,里面有账户名称以及账户金额,里面提供一个withdraw()的提款方法,之后用创建Processer()类实现Runnable接口实现多线程
在构造器中加入账户类,run()方法中使用提款方法,在主方法中创建一个新账户,创建多线程,我们为了能够实现会出现安全问题,在withdraw()方法中加了一个1s的sleep延时,
这样当t1线程,或者t2线程执行时,一个线程执行后,延时1s内,另一个线程会运行这个方法,另一个线程取到的账户余额就不是第一个线程剩下的余额。而是最开始的余额,我
们看到最后的结果是t1去了1000 t2取了1000 但是还剩4000元
public class _00线程的同步加锁 { public static void main(String[] args) throws Exception{ //创建一个公共账户 Account act=new Account("actno--01",5000.0); //创建线程对同一个账户取款 Porcessor p=new Porcessor(act); Thread t1=new Thread(p); Thread t2=new Thread(p); t1.start(); //t1.sleep(2000); t2.start(); } } //取款线程 class Porcessor implements Runnable{ //账户 Account act; //Constructor Porcessor(Account act){ this.act=act; } public void run(){ act.withdraw(1000.0); System.out.println("取款1000成功,余额:"+act.getBalance()); } } //账户 class Account{ private String actno; private double balance; public Account(){}; public Account(String actno,double balance){ this.actno=actno; this.balance=balance; } //setter and getter方法 public String getActno(){return this.actno;} public void setActno(String actno){this.actno=actno;} public double getBalance(){return this.balance;} public void setBalance(double balance){this.balance=balance;} //对外提供一个取款的方法 public void withdraw(double money){//对当前账户进行取款 double before=this.balance; double after=before-money; //延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新 this.setBalance(after); } }
运行结果:
取款1000成功,余额:4000.0
取款1000成功,余额:4000.0
3.使用线程同步 synchronized关键字
同步原理:
原理:t1线程和t2线程,t1线程执行到withdraw()方法里,遇到了synchronized关键字,就会去找this对象锁(本质是每个对象上都有一个0和1的标志,我们形象的称它为锁),如果找到this对象锁则进入同步语句块中执行程序,当同步语句块中的代码结束之后,t1线程归还this的对象锁。
在本程序中,t1线程执行同步语句块的过程中,如果t2也过来执行以下代码,也遇到了synchronized关键字,也会去找this的对象锁,但是该对象锁被t1持有,只能在这等待this对象锁归还才能执行t2线程,
也就是说t1线程在sleep()1s的时候,t2线程也执行到这里,发现t1还持有锁,只能等t1线程把账户中的钱都减掉才可以执行t2线程的代码。我们发现运行的结果是正确的。
但我们在运行的过程中会发现,运行的时间变长了。因为t2线程会等待。
class Account01{ private String actno; private double balance; public Account01(){}; public Account01(String actno,double balance){ this.actno=actno; this.balance=balance; } //setter and getter方法 public String getActno(){ return this.actno; } public void setActno(String actno){ this.actno=actno; } public double getBalance(){ return this.balance; } public void setBalance(double balance){ this.balance=balance; } //对外提供一个取款的方法 //public void synchronized withdraw(double money){ //} synchronized添加到成员方法上,线程拿走的也是this对象锁 public void withdraw(double money){//对当前账户进行取款 //把需要同步的代码,放到同步语句块中 synchronized(this){ double before=this.balance; double after=before-money; //延迟 try{Thread.sleep(1000);}catch(Exception e){} //更新 this.setBalance(after); } } }
运行结果:
取款1000成功,余额:4000.0
取款1000成功,余额:3000.0
4.使用setDaemon()方法,添加守护线程。
从线程类型上分可以分为,用户线程(以上讲的都是用户线程),另一个是守护线程。守护线程是这样的,所有的用户线程结束生命周期,守护线程才会结束生命周期
只要有一个用户线程存在,那么守护线程就不会结束,例如java中著名的垃圾回收器就是一个守护线程,只有应用程序中所有的线程结束,它才会结束,
守护线程一般都是无限执行的。我们可以使用setDaemon()方法将一个线程变为守护线程
下面的程序如果没有setDaemon()方法,那么t1线程会一下运行下去,但是加了setDaemon()方法,t1变成守护线程,当主线程运行完后,t1线程就会结束。
public class _00守护线程 { public static void main(String[] args) throws Exception{ Thread t1=new Processor(); t1.setName("守护线程"); //将t1这个用户线程修改成守护线程 t1.setDaemon(true); t1.start(); for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+"-->"+i); Thread.sleep(1000); } } } class Processor extends Thread{ public void run(){ int i=0; while(true){ i++; System.out.println(Thread.currentThread().getName()+"-->"+i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }