1.线程实现
1.继承Thread类
构造方法:
案例代码:
public class Ex10_1_CaseThread extends Thread {// 创建一个类继承(extend)Thread类 String studentName; public Ex10_1_CaseThread(String studentName) {// 定义类的构造函数,传递参数 System.out.println(studentName + "申请访问服务器"); this.studentName = studentName; } public void run() {// 用需在此线程中执行的代码覆盖Thread类的run()方法 for (int i = 0; i < 5; i++) { System.out.println("当前的服务对象是" + studentName + "同学"); try { Thread.sleep((int) (Math.random() * 2000)); } catch (InterruptedException ex) { System.err.println(ex.toString()); } }// for }// run public static void main(String[] args) { Ex10_1_CaseThread t1 = new Ex10_1_CaseThread("张三"); // 用new实例化对象 Ex10_1_CaseThread t2 = new Ex10_1_CaseThread("李四"); t1.start(); // 调用该对象的start()方法启动线程。 t2.start(); } // main }// class
结果:
张三申请访问服务器
李四申请访问服务器
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是李四同学
2.实现Runnable接口
(1)创建一个类实现Runnable接口
(2)用需在此线程中执行的代码覆盖Thread类的run方法
(3)类中定义一个Thread类对象
(4)用第二个构造方法实例化(3)对象
(5)调用对象的start()启动线程
package Thread; public class Ex10_1_CaseRunnable implements Runnable {// 创建一个类实现(implements)Runnable接口 String studentName; public Ex10_1_CaseRunnable(String studentName) {// 定义类的构造函数,传递参数 System.out.println(studentName + "申请访问服务器"); this.studentName = studentName; } public void run() {// 用需在此线程中执行的代码覆盖Thread类的run()方法 for (int i = 0; i < 5; i++) { System.out.println("当前的服务对象是" + studentName + "同学"); try { Thread.sleep((int) (Math.random() * 2000)); } catch (InterruptedException ex) { System.err.println(ex.toString()); } }// for }// run public static void main(String[] args) { Thread t1 = new Thread(new Ex10_1_CaseRunnable("张三")); // 用new实例化对象 Thread t2 = new Thread(new Ex10_1_CaseRunnable("李四")); t1.start(); // 调用该对象的start()方法启动线程。 t2.start(); } // main }
结果:
张三申请访问服务器
李四申请访问服务器
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是张三同学
当前的服务对象是李四同学
当前的服务对象是李四同学
当前的服务对象是张三同学
当前的服务对象是李四同学
2.线程的状态:
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
1.新建状态(New):
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
2.就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3.运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
4. 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
......
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5. 死亡状态(Dead)
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
3.线程的基本控制
/** * Java线程:线程的调度-优先级 * * @author*/ public class Test { public static void main(String[] args) { Thread t1 = new MyThread1(); Thread t2 = new Thread(new MyRunnable()); t1.setPriority(10); t2.setPriority(1); t2.start(); t1.start(); } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyRunnable implements Runnable { public void run() { for (int i = 0; i < 10; i++) { System.out.println("线程2第" + i + "次执行!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
线程1第0次执行!
线程2第0次执行!
线程2第1次执行!
线程1第1次执行!
线程2第2次执行!
线程1第2次执行!
线程1第3次执行!
线程2第3次执行!
线程2第4次执行!
线程1第4次执行!
线程1第5次执行!
线程2第5次执行!
线程1第6次执行!
线程2第6次执行!
线程1第7次执行!
线程2第7次执行!
线程1第8次执行!
线程2第8次执行!
线程1第9次执行!
线程2第9次执行!
Process finished with exit code 0
4.线程的主要方法:
参考JDK的API类Thread,可以获得线程名,优先级等
-
join方法:
Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。
join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。
package cn.qlq.threadTest; /** * 原生的线程类Thread的使用方法 * * @author Administrator * */ public class MyThread extends Thread { /** * 更改线程名字 * @param threadName */ public MyThread(String threadName) { this.setName(threadName); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"-----"+i); } } public static void main(String[] args) { MyThread mt = new MyThread("t1"); MyThread mt1 = new MyThread("t2"); MyThread mt2 = new MyThread("t3"); mt.start(); /**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是: 程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕 所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会 */ try { mt.join(); } catch (InterruptedException e) { e.printStackTrace(); } if(mt1.isAlive()){ System.out.println("mt1 is alive"); }else{ System.out.println("mt1 is not alive"); } mt1.start(); mt2.start(); } }
结果:
t1-----0
t1-----1
t1-----2
t1-----3
t1-----4
t1-----5
t1-----6
t1-----7
t1-----8
t1-----9
mt1 is not alive
t2-----0
t3-----0
t2-----1
t3-----1
t2-----2
t3-----2
t2-----3
t3-----3
t2-----4
t3-----4
t2-----5
t3-----5
t2-----6
t3-----6
t2-----7
t3-----7
t2-----8
t3-----8
t2-----9
t3-----9
- sleep方法
sleep(long)使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级的线程有执行的机会;
sleep(long)是不会释放锁标志的。
会抛出中断异常
补充:
Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
- yield()没有参数
sleep 方法使当前运行中的线程睡眠一段时间,进入阻塞状态,这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。
yield()也不会释放锁标志。
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
yield()只能使同优先级或更高优先级的线程有执行的机会。
实际上,yield()方法对应了如下操作;先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给次线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。
sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程此时获取CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,也没有受到I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,方可有机会运行。
yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会。
5.线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突的严重问题。当;两个以上线程访问同一个变量(全局或静态变量)的时候,并且一个线程需要修改这个变量的时候,如果不加以控制,将会带来数据不一致的问题,Java采用如下方法解决这类问题:
5.1 同步方法与同步块
1.同步方法:synchronized方法(重要)
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
例如:
public synchronized void accountAccess(int num, boolean k) { for (int i = 0; i < 3; i++) { accessType = Thread.currentThread().getName(); if (k) fund += num; else { fund -= num; } try { System.out.println("当前线程是" + accessType + ",账户剩余资金为" + fund + "。"); Thread.sleep(2000); } catch (InterruptedException ex) { System.err.println(ex.toString()); } } }
2.同步块:synchronized 块
即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
语法:
synchronized(object){ }
例如:
public void save1(int money) { synchronized (this) { account += money; } }
关于synchronized的更详细的使用方法参考:https://www.cnblogs.com/qlqwjy/p/8657950.html
5.2 wait与notify(重要)
wait():使一个线程处于等待状态,并且释放所持有的对象的lock(Object的方法)。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。不会释放锁(Thread的静态方法)。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notifyAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
例如:
public class Ex10_4_seatorderedCase { private int seatResource; // 共享缓冲区 private boolean empty = true; // seatResource是否为空的信号量 public void setEmpty(){ empty=true; } public synchronized void push(int pubResource) { while (!empty) { // 当缓冲区满的时候,等待 try { // 阻塞自己 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } seatResource = pubResource; // 将生成的座位号放到缓冲区 empty = false; // 设置缓冲区满状态 notify(); // 唤醒其他等待线程 } public synchronized int pop() { // 从缓冲区订座位 while (empty) { try { wait(); // 当缓冲区空的时候,等待 } catch (InterruptedException e) { e.printStackTrace(); } } int popResource = seatResource; seatResource = 0; empty = true; // 设置缓冲区空状态 notify(); return popResource; // 返回所订座位号 } public static void main(String[] args) { Ex10_4_seatorderedCase so = new Ex10_4_seatorderedCase(); SeatProcedure sp = new SeatProcedure(so); sp.start(); SeatConsumer sc = new SeatConsumer(so); sc.start(); SeatRelease sr=new SeatRelease(so); sr.start(); } } class SeatProcedure extends Thread { //生成空座位线程 private Ex10_4_seatorderedCase so; public SeatProcedure(Ex10_4_seatorderedCase so) { this.so = so; } public void run() { for (int i = 1; i <= 30; i++) { //连续向缓冲区生成空座位号 int pubResource = i; so.push(pubResource); System.out.println("第" + pubResource + "号座位为空"); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }//class end class SeatConsumer extends Thread{ //预订座位线程 private Ex10_4_seatorderedCase so; public SeatConsumer(Ex10_4_seatorderedCase so) { this.so= so; } public void run() { for (int i = 1; i <= 50; i++) {//50个学生连续从缓冲区取出座位号 synchronized (so) { int sh = so.pop(); if (sh != 0) { System.out.println("学生" + i + " "+"占了第" + sh+"号座位"); } else { System.out.println("没有空座,请等待!"); } } try { sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }//class end class SeatRelease extends Thread { //释放座位线程 private Ex10_4_seatorderedCase so; public SeatRelease(Ex10_4_seatorderedCase so) { this.so = so; } public void run() { try { sleep(20000);//20秒后 this.so.setEmpty(); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 1; i <= 30; i++) { //从第一个开始,连续释放已预订的座位 int pubResource = i; so.push(pubResource); System.out.println("第" + pubResource + "号座位取消预订"); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
第1号座位为空
学生1 占了第1号座位
第2号座位为空
学生2 占了第2号座位
第3号座位为空
学生3 占了第3号座位
第4号座位为空
学生4 占了第4号座位
第5号座位为空
学生5 占了第5号座位
第6号座位为空
学生6 占了第6号座位
第7号座位为空
学生7 占了第7号座位
第8号座位为空
学生8 占了第8号座位
第9号座位为空
学生9 占了第9号座位
第10号座位为空
学生10 占了第10号座位
第11号座位为空
学生11 占了第11号座位
第12号座位为空
学生12 占了第12号座位
第13号座位为空
学生13 占了第13号座位
第14号座位为空
学生14 占了第14号座位
第15号座位为空
学生15 占了第15号座位
第16号座位为空
学生16 占了第16号座位
第17号座位为空
学生17 占了第17号座位
第18号座位为空
学生18 占了第18号座位
第19号座位为空
学生19 占了第19号座位
第20号座位为空
学生20 占了第20号座位
第21号座位为空
学生21 占了第21号座位
第22号座位为空
学生22 占了第22号座位
第23号座位为空
学生23 占了第23号座位
第24号座位为空
学生24 占了第24号座位
第25号座位为空
学生25 占了第25号座位
第26号座位为空
学生26 占了第26号座位
第27号座位为空
学生27 占了第27号座位
第28号座位为空
学生28 占了第28号座位
第29号座位为空
学生29 占了第29号座位
第30号座位为空
学生30 占了第30号座位
5.3 使用重入锁实现线程同步 (重要)
ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁 unlock() : 释放锁
//只给出要修改的代码,其余代码与上同 class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } }
5.4 使用特殊域变量(volatile)实现线程同步
5.5 使用局部变量实现线程同步
5.6 使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。 本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步 LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。 队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~LinkedBlockingQueue 类常用方法 LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue put(E e) : 在队尾添加一个元素,如果队列满则阻塞 size() : 返回队列中的元素个数 take() : 移除并返回队头元素,如果队列空则阻塞代码实例: 实现商家生产商品和买卖商品的同步
注:BlockingQueue<E>定义了阻塞队列的常用方法,尤其是四种操作元素的方法,我们要多加注意,当队列满或空时:
add()方法会抛出异常
offer()方法返回false
take()方法会阻塞
put()方法会阻塞
5.7使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的
AtomicIntegeraddAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
代码实例:
只改Bank类,其余代码与上面第一个例子同
class Bank { private AtomicInteger account = new AtomicInteger(100); public AtomicInteger getAccount() { return account; } public void save(int money) { account.addAndGet(money); } }
补充--原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。
5.8 使用特殊域变量(volatile)实现线程同步
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
package com.dxz.volatiledemo; //只给出要修改的代码,其余代码与上同 class Bank { // 需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } // 这里不再需要synchronized public void save(int money) { account += money; } } package com.dxz.volatiledemo; import java.util.concurrent.CountDownLatch; public class MyThread implements Runnable { Bank bank; CountDownLatch cdl; MyThread(Bank bank, CountDownLatch cdl) { this.bank = bank; this.cdl = cdl; } @Override public void run() { for(int i = 1; i < 101; i++) { bank.save(i); } cdl.countDown(); } } package com.dxz.volatiledemo; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class Client { public static void main(String[] args) throws InterruptedException { int num = 70; ExecutorService es = Executors.newFixedThreadPool(num); Bank bank = new Bank(); CountDownLatch cdl = new CountDownLatch(num); for(int i = 0; i<num;i++) { MyThread mt = new MyThread(bank, cdl); es.submit(mt); //TimeUnit.SECONDS.sleep(1); } cdl.await(); es.shutdown(); System.out.println(bank.getAccount()); } }
每个线程间隔一秒时,计算结果是:353600,如果去掉线程间隔让线程竞争起来,结果很多时候不一致。验证了volatile变量的以下特性:
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。