1.线程的基本概念
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态 的过程:有它自身的产生、存在和消亡的过程。——生命周期
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开 销小
一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以 访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资 源可能就会带来安全的隐患。
- 关系
进程可以细化为多个线程
每个线程拥有自己独立的:栈区和程序计数器
多个线程共享一个程序中结构:方法区、堆
2.使用多线程的优点
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方 法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢? 多线程程序的优点: 1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。 2. 提高计算机系统CPU的利用率 3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和 修改
3.线程的使用
继承Thread类的方式
实现Runnable接口的方式
3.1方式1 Thread方式
1) 定义子类继承Thread类。
2) 子类中重写Thread类中的run方法。
3) 创建Thread子类对象,即创建了线程对象。
4) 调用线程对象start方法:启动线程,调用run方法。
- 注意点:
1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。
3. 想要启动多线程,必须调用start方法。
4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。
- 代码演示
/**
* 常用的方法
* 1.start 启动线程,调度run方法
* 2.run 线程的执行内容
* 3.currentThread() 静态方法,返回执行当前代码的线程
* 4.getName() 获取当前线程的名称
* 5.setName() 设置当前线程的名称
* 6.yield() 释放当前cpu的执行权,但是下次一cpu任然可能抢占
* 7.join() 在线程1调用线程2的join方法,此时线程1进入阻塞状态!!
* 直到线程2 执行完毕释放了 才结束线程1 的阻塞状态
* 8.stop() 执行此方法强行结束当前线程
* 9. sleep(long time) 让线程指定睡眠time时间,阻塞time时间过了继续进行线程任务
* 10 isAlive() 判断当前线程是否存活
*
* **************************
* 线程的优先级问题
* 1.
* MIN_PRIORITY = 1
* NORM_PRIORITY = 5 (默认优先级)
* MAX_PRIORITY = 10
* 2.设置优先级
* getPriority()获取优先级
* setPriority(int p) 设置优先级
*
* 说明:高优先级的进程会抢占地优先级的cpu资源的执行权力
* 但是只是意味着高优先级的进程抢占成功率更好不代表高优先级
* 先执行进程才到低优先级进程执行。
*/
public class demoCreat {
public static void main(String[] args) throws InterruptedException {
// 3.创建thread类
MyThread myThread = new MyThread();
MyThread myThread1 = new MyThread();
/**
* 设置优先级
*/
myThread.setPriority(Thread.MAX_PRIORITY);
myThread1.setPriority(Thread.MIN_PRIORITY);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
/**
* 不可以直接调用run方法这样不是线程的启动方式
* 。start的作用就是 1.启动线程 2.调用当前线程的run方法
* Thread.currentThread().getName() 获取线程名称
*/
// myThread.run();//切换运行,发现线程的main和Mythread名称都是一致的
myThread.start();
/**
* 同一个对象的线程的start方法不能被多次调用
* 可以用创建新对象的方式调用start方法
*/
myThread.setName("我的线程000");
// myThread.run();//错误
myThread1.start();
myThread1.setName("我的线程111");
//main线程调度
Thread.currentThread().setName("main线程");
System.out.println("myThread存活状态:"+myThread.isAlive());
// myThread1.join();//表示main线程阻塞,其他线程交替执行
for (Integer i=0;i<100;i++){
if (i%2==0) System.out.println(i+":"+Thread.currentThread().getName());
}
}
}
//1.创建 一个集成于thread类
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
public MyThread() {
}
/**
* 2.
*从写thread类的run方法
* 根据项目的实际功能编辑
* 这里做测试 就打印0-100的偶数
*/
@Override
public void run() {
for (Integer i=0;i<100;i++){
// try {
// sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (i%2==0) System.out.println(i+":"+Thread.currentThread().getName());
}
}
}
3.2Runnable方式
1) 定义子类,实现Runnable接口。
2) 子类中重写Runnable接口中的run方法。
3) 通过Thread类含参构造器创建线程对象。
4) 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
5) 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
代码演示
package com.dfp.dome1;
/**
* 创建多线程方式2:实现Runnable接口
* 1.创建一个类实现runnable接口
* 2.实现run方法
* 3.创建此类的对象
* 4.这个类作为参数传递给thread类中的构造器,创建thread类的对象
* 5.通过thread类的对象调用start方法启动线程
*/
public class RunnableThread {
public static void main(String[] args) {
RThread rThread=new RThread();
Thread thread=new Thread(rThread);
/**
* 对线程的操作比如修改属性这些都是通过 thread.的方式来进行
*/
thread.start();
}
}
class RThread implements Runnable{
@Override
public void run() {
for (Integer i=0;i<100;i++){
if (i%2==0) System.out.println(i+":"+Thread.currentThread().getName());
}
}
}
3.3 抢票案例演示
/**
* 创建线程的2种方式的比较
* 开发中有限选择实现Runnable接口的方式
* 1.实现的方式没有类的单继承的局限性
* 2.实现的方式更合适处理多个线程共享数据的情况
* 相同:
* 都需要重写run方法
* 线程的逻辑处理都需要在run方法中实现
*/
public class WindowsDome {
public static void main(String[] args) {
DomeRunnable domeRunnable=new DomeRunnable();
Thread t1=new Thread(domeRunnable);
Thread t2=new Thread(domeRunnable);
Thread t3=new Thread(domeRunnable);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class DomeRunnable implements Runnable{
private Integer tick=100;
@Override
public void run() {
while (true){
if (tick>0){
System.out.println(Thread.currentThread()
.getName()+"售出车票为:"+tick);
tick--;
}else {
break;
}
}
}
}
结果
我们发现有个问题 票号100的同时被消费了。但是实际情况是不允许的
如果我们让窗口的线程沉睡了,还会出现-1号的票号问题(小概率概率出现)
4.线程的生命周期
4.1生命周期的转换
5.线程同步
1. 多线程出现了安全问题
2. 问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
3. 解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以 参与执行。
5.1线程同步的方式
5.2 同步锁
- 同步机制中的锁
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this) 同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就 无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)
5.3 方式一同步代码块演示
/**
*问题 描述 出票过程中 出现重票/错票等问题
* 原因:当某个线程执行的过程中。尚未操作完成的时候。其他线程也参与进来操作
* 解决方式:当线程a操作第一个票号100的时候 ,其他的线程不能参与进来操作票号100
* 直至a线程完成任务其他线程才可以操作票号。即使a线程阻塞其他线程也是不能参与。
* java解决线程冲突的方法
* 方式1:同步代码块
* synchronized(监视器){}
* 1.多线程操作的共享代码(共享数据)视为同步代码
* 2.共享数据
* 3.同步监视器,我们俗称锁,任何一个对象都称之为一把锁(多个线程必须共享一把锁)
* 方式2:同步方法
*/
public class WindowsDome {
public static void main(String[] args) {
DomeRunnable domeRunnable=new DomeRunnable();
Thread t1=new Thread(domeRunnable);
Thread t2=new Thread(domeRunnable);
Thread t3=new Thread(domeRunnable);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class DomeRunnable implements Runnable{
private Integer tick=100;
Object object=new Object();//用做锁的对象,注意定义的位置保证唯一性
@Override
public void run() {
while (true){
//设置同步代码块,并且我们的DomeRunnable之创建一次意味着 objec对象也是创建一次。这么说在3个线程中object是唯一的
synchronized (object){
if (tick>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()
.getName()+"售出车票为:"+tick);
tick--;
}else {
break;
}
}
}
}
}
5.4 同步方法演示
修改如下
run方法中调用show方法
5.5 继承Thread的方式实现同步
因为通过继承thread的方式我们实现线程,这个方式创建一个线程就会创建3个类对于我的锁的机制而言就会冲突。我们主要修改一下这几个地方
将我们的锁设置为静态数据这样就是唯一的
或者在我们的同步方法加上静态
public class WindowsDomeThread {
public static void main(String[] args) {
WindowExThread t1=new WindowExThread();
WindowExThread t2=new WindowExThread();
WindowExThread t3=new WindowExThread();
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
class WindowExThread extends Thread{
private static Integer tick =100;
private static Object lock=new Object();//静态
@Override
public void run() {
while (true){
// synchronized (lock){
//
// if (tick>0){
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread()
// .getName()+"售出车票为:"+tick);
// tick--;
// }else {
// break;
// }
// }
show();
}
}
private static synchronized void show(){
if (tick>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()
.getName()+"售出车票为:"+tick);
tick--;
}
}
}
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程
5.6 Lock锁实现同步
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。
import java.util.concurrent.locks.ReentrantLock;
/**
* jdk5新增方法 lock锁
* synchronized 和lock
* 相同:解决线程安全问题
* 不同:synchronized机制在执行完成代码同步之后,自动释放监听器(同步代码块和同步方法)
* lock需要手动 启动同步(lock())和结束同步(unlicok())
*/
public class JDK5Lock {
public static void main(String[] args) {
Window domeRunnable=new Window();
Thread t1=new Thread(domeRunnable);
Thread t2=new Thread(domeRunnable);
Thread t3=new Thread(domeRunnable);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private Integer tick=100;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
lock.lock();//上锁
try {
while (true){
if (tick>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()
.getName()+"售出车票为:"+tick);
tick--;
}else {
break;
}
}
}finally {
lock.unlock();//解锁
}
}
}
6.线程通信
代码演示
import javax.swing.*;
/**
* 线程通信的例子:使用2个线程打印1-100,线程1/2交替打印
* 涉及到的方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并且释放同步监听器
* notify():一旦执行方法,就会唤醒一个被wait的线程。如果有多个被wait,优先级高的先唤醒
* notifyAll():一旦执行唤醒全部被wait的线程
* 说明:
* 1.三个方法必须使用在同步代码块中或者同步方法中
* 2.三个方法的调用者必须是同步代码块或同步方法中的同步监视器
* 3.3个方法定义在 Object中
* sleep和wait
* 不同点:2个方法执行的位置不同,Thread类中声明sleep object中声明wait
* 调用的要求不同,sleep可以在所以场景下调用,wait必须在同步代码下调用
* 如果2个方法都使用在同步代码块和方法中,sleep不会释放锁,wait会释放锁
*/
public class correspondence{
public static void main(String[] args) {
Number number=new Number();
Thread t1=new Thread(number);
Thread t2=new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number=1;
@Override
public void run() {
while (true){
synchronized (this){
this.notify();//唤醒被阻塞的线程,调用者是当前监视器的对象
// this.notifyAll();
if (number<=100){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();//让当前线程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
案例演示
public class ProductDome {
public static void main(String[] args) {
Clerk clerk=new Clerk();
Product product1=new Product(clerk);
Product product2=new Product(clerk);
Consumer consumer=new Consumer(clerk);
product1.setName("生产者1");
product2.setName("生产者2");
consumer.setName("消费者1");
product1.start();
product2.start();
consumer.start();
}
}
class Clerk {
//商品类
private Integer GoodsCount=0;
//生产产品
public synchronized void ProduceGoods() {
//设置同步方法 防止线程的安全问题
if (GoodsCount<20){
GoodsCount++;
System.out.println(Thread.currentThread().getName()
+":开始生产第:"+GoodsCount);
// 当数量可加 通知阻塞的消费者线程 唤醒
notifyAll();
}else {
try {
/**
* 当数量为20设置阻塞
*/
wait();
}catch (Exception e){
e.printStackTrace();
}
}
}
//消费产品
public synchronized void ConsumeProduct() {
//设置同步方法 防止线程的安全问题
if (GoodsCount>0){
System.out.println(Thread.currentThread().getName()
+":开始消费第:"+GoodsCount);
GoodsCount--;
//当数量可减并且 已经减少了 可以通知阻塞的生产者线程 进行唤醒
notifyAll();
}else {
try {
// 当数量为负数设置阻塞
wait();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
//生产者进程
class Product extends Thread{
private Clerk clerk;
public Product(Clerk clerk){
this.clerk=clerk;
}
@Override
public void run() {
while (true){
try {
sleep(90);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.ProduceGoods();
}
}
}
//消费者进程
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk=clerk;
}
@Override
public void run() {
while (true){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.ConsumeProduct();
}
}
}
7.创建线程的其他方式
7.1 实现Callable 接口
实现callable接口通过FutureTask接口实现callbale。
Future接口
可以对具体Runnable、Callable任务的执行结果进行取消、查询是 否完成、获取结果等。
FutrueTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为 Runnable被线程执行,又可以作为Future得到Callable的返回值
代码实现
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* callable和Runnable比较的优势
* 1.call可以带返回值
* 2.call可以抛出异常
* 3.callable是支持泛型的
*/
public class CreatCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3.创建cabble实现的接口
CallableThread callableThread=new CallableThread();
// 4.将cabble对象传递给FutureTask创建对象
FutureTask<Integer> futureTask=new FutureTask<Integer>(callableThread);
// FutureTask futureTask=new FutureTask<>(callableThread);
// 5.将FutureTask传递给thread对象启动线程
new Thread(futureTask).start();
/**
* get的返回值来自于 FutureTask构造器传递的参数对象Callable类中重写的call方法的返回值
* 如果需要明确的定义返回值的类型需要在new FutureTask<>中显示的把泛型的类型定义
* 默认也是Object类型
*/
Integer sum = futureTask.get();
System.out.println("总和为"+sum);
}
}
//1.创建一个类实现Callable
class CallableThread implements Callable<Integer>{
Integer sum=0;
/**2.重写call方法,将线程需要执行的任务或者逻辑代码声明在call中可以带返回值
* call方法可以带返回值
* call的返回值的类型和实现类中定义的类一致(默认为Object)
*/
@Override
public Integer call() throws Exception {
for (Integer i=0;i<=100;i++){
if (i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
7.2使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
创建方式:
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行 Callable
void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。
/**
* 优势
* 1.提高响应时间
* 2.减少资源消耗
* 3.便于线程管理
* setMaximumPoolSize:最大线程数量
* setCorePoolSize:核心池大小
* setKeepAliveTime:线程没有响应或者没有任务的最大等待时间
*
*/
public class PoolCreat {
public static void main(String[] args) {
ExecutorService service= Executors.newFixedThreadPool(10);
//通过ExecutorService接口的实现类设置属性
//获取实现类
System.out.println(service.getClass());
ThreadPoolExecutor serviceImpl= (ThreadPoolExecutor) service;
serviceImpl.setMaximumPoolSize(500);
serviceImpl.setCorePoolSize(300);
serviceImpl.setKeepAliveTime(100, TimeUnit.MICROSECONDS);//数值,单位
//使用于callable接口
//service.submit();
//使用于Runnable接口 启动2个不同的线程
service.execute(new NumThread());
service.execute(new stringThread());
//关闭连接池
service.shutdown();
}
}
class NumThread implements Runnable{
@Override
public void run() {
for (Integer i= 0;i<=100;i++){
if (i%2!=0){
System.out.println(i);
}
}
}
}
class stringThread implements Runnable{
@Override
public void run() {
for (Integer i= 0;i<=100;i++){
if (i%2!=0){
System.out.println("aaa");
}
}
}
}