Java中线程的使用
基本概念:程序
、进程
、线程
- 程序:一段静态的代码
- 进程:一段运行的代码
- 线程:进程的细分,是一个进程内部的一条执行路径,每个线程,拥有自己独立的:栈、程序计数器,多个线程,共享同一个进程中的结构:方法区、堆
具体对于三者之间的关系可以去学习计算机操作系统中相关章节
CPU的分类
- 单核CPU
- 多核CPU
并行与并发
- 并行:同一时间
点
发生多个事件 - 并发:同一时间
段
发生多个事件
一个Java应用程序java.exe,其实至少有三个线程:
main()主线程
,gc()垃圾回收线程
,异常处理线程
。当然如果发生异常,会影响主线程
线程(Thread)
的创建
方式一:继承Thread类的方式
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
public class ThreadTest extends Thread{ // 1.继承于Thread类的子类
public static void main(String[] args) {
ThreadTest test = new ThreadTest(); // 3.创建Thread类的子类的对象
ThreadTest2 test2 = new ThreadTest2();
test.start(); // 4.通过此对象调用start() ①启动当前的线程 ② 调用当前的线程run()
// ThreadTest2的线程
test2.start();
// test.run(); // 直接调用run方法只能是让run方法在main线程里面,不能独立出新线程
System.out.println(Thread.currentThread().getName());
}
@Override
public void run() { // 2.重写Thread类的run()
for (int i = 0; i < 100 ; i++) {
if (i % 2 == 0){
System.out.println(i + " : " + Thread.currentThread().getName());
}
}
}
}
class ThreadTest2 extends Thread{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i % 2 != 0){
System.out.println(i+" : "+Thread.currentThread().getName());
}
}
}
}
注:一个线程(Thread)的创建,
只能使用一次对象.start()
,如果一个对象再一次调用start方法,
会报错:java.lang.IllegalThreadStateException
Thread类中的常见方法
方法名 | 作用 |
| 启动当前线程;调用当前线程的run() |
| 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 |
| 静态方法,返回执行当前代码的线程 |
| 获取当前线程的名字 |
| 设置当前线程的名字 |
| 释放当前cpu的执行权 |
| 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态,继续执行 |
|
|
| 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态(不执行) |
| 判断当前线程是否存活(是否为运行态) |
线程优先级中的方法
- 线程中定义好的的优先等级:
1 ~ 10
属性名 | 作用 |
| 线程最大优先级 |
| 线程最小优先级 |
| 自定义线程默认优先级(默认为5) |
- 线程优先级中的方法
方法名 | 作用 |
| 获取线程的优先级 |
| 设置线程的优先级 |
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从
概率
上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行
方式二:实现Runnable接口的方式
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
public class RunnableTest {
public static void main(String[] args) {
Runnable1 runable1 = new Runnable1(); // 2. 实现类去实现Runnable中的抽象方法:run()
Thread run1= new Thread(runable1); // 3. 创建实现类的对象 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
run1.start(); // 5. 通过Thread类的对象调用start() ①启动当前线程 ② 调用当前线程的run()---> 调用了Runnable类型中的target的run()
Thread run2= new Thread(runable1);
run2.start();
}
}
class Runnable1 implements Runnable{ // 1. 创建一个实现了Runnable接口的类
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" : " + i);
}
}
}
Thread继承
创建线程和Runnable实现
线程之间的比较
- 相同点:
- 都需要
重写run方法
- 都需要执行start方法来执行线程
- public class Thread implements Runnable
- 不同点:
- Thread通过继承的方式重写Thread中的run方法,而runnable通过接口去实现接口中的抽象方法run重写
- Thread继承创建的线程简单,但是存在局限性,Runnable实现的线程创建过程相对较麻烦,但好在接口的多实现性
总结
- 开发中创建线程优先采用
Runnable
来创建线程到方式 -
Runnable
实现的方式没类的单继承性的局限性 - 实现的方式更适合来处理多个线程共享数据的情况
线程的生命周期
- 新建
- 就绪
- 运行
- 阻塞
- 结束
线程的同步(线程安全性问题)
- 举例:在生活中银行取钱和火车买票过程中可会出现取钱重复或者买票票号重复情况,这类情况是由于系统线程所引起的
- 解决:当一个线程操作一个公共数据时,其他线程等待该线程操作结束后,继续操作公共数据
java中通过同步机制,来解决线程的安全问题
- 方式一:同步代码块
- ①.操作
共享数据的代码
,就是包裹需要被同步的代码。 --> 不能包含代码多了,也不能包含代码少了 - ②.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
- ③.同步监视器,俗称:
锁
。任何一个类的对象,都可以充当锁
- 要求:多个线程必须要共用同一把锁
synchronized(同步监视器){
// 同步代码
}
// -----------------------------------------------
public class Windows2 implements Runnable{
public static void main(String[] args) {
Windows2 windows1 = new Windows2();
Thread win1 = new Thread(windows1);
Thread win2 = new Thread(windows1);
Thread win3 = new Thread(windows1);
win1.setName("窗口1");
win2.setName("窗口2");
win3.setName("窗口3");
win1.start();
win2.start();
win3.start();
}
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized (this){ // 将需要同步的ticket包起来 this就是代表一个公共锁
// synchronized (Window2.class){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + " : " + ticket);
ticket--;
}else{
break;
}
} // synchronized 结束
}
}
}
- 补充:
在实现Runnable接口创建多线程的方式中,我们可以考虑使用
this
充当同步监视器
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(类名.class)
充当同步监视器
- 方式二:同步方法
- 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身 –
public class Windows extends Thread {
public static void main(String[] args) {
Windows window1 = new Windows();
Windows window2 = new Windows();
Windows window3 = new Windows();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
private static int ticket = 100;
private static boolean isBtn = true; // 控制循环的结束
@Override
public void run() {
while( isBtn ){
show();
}
}
private static synchronized void show(){
// 使用同步方法的方式 同定义ticket时一样同步需要一把锁,当方法设置为static时,它指向的就是本类Windows2.class,
// 如果不设置static则指向this,也就是说三个对象各自指向自己的this,就不存在线程同步了,存在线程安全问题
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+" : "+ticket);
ticket--;
}else{
isBtn =false;
}
}
}
- 方式三:Lock锁 — JDK5.0新增
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- Lock锁创建过程
- 在类中实现接口 ReentrantLock lock = new ReentrantLock()
- 将需要同步的代码放入try{}finally{}中
- 在try中开头设置锁 lock.lock()
- 在finally中设置解除锁 lock.unlock()
import java.util.concurrent.locks.ReentrantLock;
public class WindowLock implements Runnable{
public static void main(String[] args) {
WindowLock window = new WindowLock();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(); // 1.实现接口
@Override
public void run() {
// lock.lock(); // 这里也可以设置锁,效果一样,区别暂时不知道
while(true){
try{ //2. 将需要同步的代码放入try{}finally{}中
lock.lock(); // 3.设置锁
if( ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +" : " + ticket);
ticket--;
} else
break;
}finally {
lock.unlock(); // 3.设置解除锁
}
}
}
}
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 使用优先顺序
- Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
线程通信
-
wait()
:令当前线程挂起
并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行 -
notify()
:唤醒
正在排队等待同步资源的线程中优先级最高者结束等待 -
notifyAll ()
:唤醒
正在排队等待资源的所有线程结束等待
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException
异常
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明
public class Window4 implements Runnable {
public static void main(String[] args) {
Window4 window = new Window4();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized (this){
notifyAll(); // 1. 线程唤醒
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+" : "+ ticket);
ticket--;
}else
break;
try {
wait(); // 1. 线程挂起
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
sleep()
和wait()
到区别
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
- 不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁
,wait()会释放锁
线程通信的应用
生产者与消费者问题
- 问题表述:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 存在问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到
- 消费者比生产者快时,消费者会取相同的数据
public class ProductTest { // 4.测试类
public static void main(String[] args) {
Clerk clerk = new Clerk(); // 构建同一个锁
Producter producter = new Producter(clerk); // 创建线程 并将clerk作为线程的锁
producter.setName("生产者");
Customer customer1 = new Customer(clerk);
customer1.setName("消费者1");
Customer customer2 = new Customer(clerk);
customer2.setName("消费者2");
producter.start();
customer1.start();
customer2.start();
}
}
class Clerk{ // 1.店员
private int count = 0; // 1.2声明产品个数
public synchronized void produceProduct() { // 生产产品 1.2. 将方法设置为同步方法
if(count < 20){ // 1.3. 判断count数量小于20就生产
count++;
System.out.println(Thread.currentThread().getName()+"生产个数:"+count);
notify(); // 1.4. 只要增加了一个产品,就可以去唤醒消费者线程去消费产品
}else{
try {
wait(); // 1.5. 当count数量到了20就要将生产者线程挂起
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void customProduct() { // 消费产品
if (count > 0){
System.out.println(Thread.currentThread().getName()+"消费个数"+count);
count--;
notify();
}else
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Producter extends Thread{ // 2. 生产者
private Clerk clerk; // 2.1 创建一个Clerk类型的变量
public Producter(Clerk clerk) { // 2.2. 构造一个带有clerk形参的构造器
this.clerk = clerk; // 2.3. 变量赋值
}
@Override
public void run() { // 2.4.重写run方法
System.out.println(getName()+" :开始生产产品");
while (true){
try {
Thread.sleep(100);// 设置生产速度
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct(); // 2.5. 调用clerk中的方法,从而修改count
}
}
}
class Customer extends Thread{ // 3.消费者
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+" :开始消费产品");
while (true){
try {
Thread.sleep(300); // 设置消费速度
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.customProduct();
}
}
}
JDK5.0中新增的线程创建方式
方式一:实现Callable接口
- 与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助
FutureTask
类,比如获取返回结果
package wsf.exam._211009.java;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 线程创建新方式Callable
*/
public class CallableTest {
public static void main(String[] args) {
NumThread numThread = new NumThread(); // 3. 创建接口实现对象
FutureTask task = new FutureTask(numThread); // 4. 若想获取numThread中的返回值需要借助FutureTask类,将创建的实现对象作为参数传递到FutureTask构造器中,创建构造器
new Thread(task).start(); // 5. 将FutureTask的实例对象task传递到Thread类的构造器中,创建Thread的实例对象,并调用start方法执行线程操作
try {
// get() 返回值FutureTask构造器参数callable实现类重写call()的返回值
Object sum = task.get();
System.out.println("sum的总和是:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1. 创建一个实现Callable接口的实现类
class NumThread implements Callable{
//2. 实现call方法,将线程需要实现的操作声明在call方法中,并且可附带返回值
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
方式二:创建线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
JDK 5.0起提供了线程池相关API:ExecutorService
和 Executors
// ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
// void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
// <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
// void shutdown() :关闭连接池
// Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
// Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
// Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
// Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
// Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
// ------------------------------------------------------------------
package wsf.exam._211009.java;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程创建方法四:创建线程池
*/
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10); // 1.创建线程池,并提供线程中最大可容量线程的个数
// 2. 执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread()); // 适合于Runnable
service.execute(new NumberThread1()); // 适合于Runnable
// service.submit(Callable callable); // 适用于Callable
service.shutdown(); // 3. 关闭线程池
}
}
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+ ":" + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName()+ ":" + i);
}
}
}
}