一、进程与线程
1、进程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
进程的本质就是一些系统内存,每个进程占据一定的资源。
2、线程
线程是指进程中的一个执行流程,进程中包含的一个或多个执行单元(子任务,线程)
一个进程中包含一到多个线程(但是不能一个都没有)。
二、线程的创建
1、方式一:继承Thread类
1.1、步骤
(1)自定义线程类继承自Thread类
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
1.2、 代码体现
//创建线程方式一:继承Thread类,重新run()方法,调用start开启线程
//总结:线程调用不一定立即执行,由CPU调度执行
public class TestThread1 extends Thread{
@Override
public void run() {
//run()方法线程体
for (int i = 0; i <200 ; i++) {
System.out.println("我在看代码"+i);
}
}
public static void main(String[] args) {
//main线程 主线程
//创建一个Thread对象
Thread testThread1 = new TestThread1();
//调用start方法开启线程
testThread1.start();
for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程"+i);
}
}
}
2、方式二:实现Runable接口
2.1、步骤
(1)自定义MyRunable类实现Runable接口
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
2.2、 代码体现
//创建线程方式二:实现Runable接口,重写run方法
public class TestThread02 implements Runnable{
@Override
public void run() {
//run()方法线程体
for (int i = 0; i <200 ; i++) {
System.out.println("我在看代码"+i);
}
}
public static void main(String[] args) {
//main线程 主线程
//创建Runable接口的实现类对象
TestThread02 testThread02 = new TestThread02();
//创建线程对象,通过线程对象来开启线程 代理
new Thread(testThread02).start();
for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程" + i);
}
}
}
3、方式三:实现Callable接口
3.1、步骤
(1)实现Callable接口,需要返回值类型
(2)重写call()方法,抛出异常
(3)创建目标对象 TestCallable t1 = new TestCallable();
(4)创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(1);
(5)提交执行 Future r1 = ser.submit(t1);
(6)获取结果 boolean rs1 = r1.get();
‘(7)关闭服务 ser.shutdownNow();
3.2、 代码体现
//创建线程方式三:实现Callable接口,重写call方法
//实现Callable接口 实现多线程同步下载图片
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() throws Exception {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downLoader(url,name);
System.out.println("下载了文件名为"+name);
return true;
}
//主线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1561383303,2055953075&fm=26&gp=0.jpg","1.jpg");
TestCallable t2 = new TestCallable("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3258014100,3780692077&fm=26&gp=0.jpg","2.jpg");
TestCallable t3 = new TestCallable("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3565814160,3345825283&fm=26&gp=0.jpg","3.jpg");
//1、创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//2、提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
//3、获取结果
boolean rs1 = r1.get();
boolean rs2 = r1.get();
boolean rs3 = r1.get();
//4、关闭服务
ser.shutdownNow();
}
}
//下载工具类
public class WebDownloader{
//url:远程路径 name:文件名字
public void downLoader(String url,String name){
try {
//FileUtils文件工具,复制文件
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,文件下载失败");
}
}
}
4、总结
开发中,优先选择实现Runable接口的方式
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合用来处理多个线程有共享数据的情况,方便同一个对象被多个线程调用
三、代理模式
代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象。
这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。
静态代理代理类在编译期就生成
动态代理代理类则是在Java运行时动态生成。
1、静态代理
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了.
静态代理角色分析
- 抽象角色 : 一般使用接口或者抽象类来实现
- 真实角色 : 被代理的角色
- 代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作
- 客户 : 使用代理角色来进行一些操作 .
代码实现
//抽象角色:租房
public interface Rent {
public void rent();
}
//真实角色: 房东,房东要出租房子
public class Host implements Rent{
public void rent() {
System.out.println("房屋出租");
}
}
//代理角色:中介
public class Proxy implements Rent {
private Host host;
public Proxy() { }
public Proxy(Host host) {
this.host = host;
}
//租房
public void rent(){
seeHouse();
host.rent();
fare();
}
//看房
public void seeHouse(){
System.out.println("带房客看房");
}
//收中介费
public void fare(){
System.out.println("收中介费");
}
}
//客户类,一般客户都会去找代理!
public class Client {
public static void main(String[] args) {
//房东要租房
Host host = new Host();
//中介帮助房东
Proxy proxy = new Proxy(host);
//你去找中介!
proxy.rent();
}
}
2、动态代理
在程序运行时,运用反射机制动态创建而成。
为什么要使用动态代理
- 代理对象的一个接口只服务于一种类型的对象,当代理的方法很多时,就要为每一种方法都进行代理,代码冗余大。
- 当接口中增加方法时,除了实现类要实现这个方法,所有代理类也需要实现此方法,增加了代码维护的复杂度。
动态代理分为两类 : 一类是基于接口动态代理 , 一类是基于类的动态代理
- 基于接口的动态代理----JDK动态代理
- 基于类的动态代理--cglib
代码实现
//抽象角色:租房
public interface Rent {
public void rent();
}
//真实角色: 房东,房东要出租房子
public class Host implements Rent{
public void rent() {
System.out.println("房屋出租");
}
}
//代理角色:中介
public class ProxyInvocationHandler implements InvocationHandler {
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
//生成代理类,重点是第二个参数,获取要代理的抽象角色!之前都是一个角色,现在可以代理一类角色
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
rent.getClass().getInterfaces(),this);
}
// proxy : 代理类 method : 代理类的调用处理程序的方法对象.
// 处理代理实例上的方法调用并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
seeHouse();
//核心:本质利用反射实现!
Object result = method.invoke(rent, args);
fare();
return result;
}
//看房
public void seeHouse(){
System.out.println("带房客看房");
}
//收中介费
public void fare(){
System.out.println("收中介费");
}
}
//租客
public class Client {
public static void main(String[] args) {
//真实角色
Host host = new Host();
//代理实例的调用处理程序
ProxyInvocationHandler pih = new ProxyInvocationHandler();
pih.setRent(host); //将真实角色放置进去!
Rent proxy = (Rent)pih.getProxy(); //动态生成对应的代理类!
proxy.rent();
}
}
3、使用场景
- 代理单个目标对象——使用静态代理
- 代理多个目标对象——使用动态代理
四、Lambda表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 避免匿名内部类定义过多
- 使用 Lambda 表达式可以使代码变的更加简洁紧凑。
- Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
- Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
1、语法
(parameters) -> expression[表达式]
或
(parameters) ->{ statements[语句]; }
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
2、使用
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
3、函数式接口
只有一个 抽象方法 (Object类中的方法除外)的接口是函数式接口
只能有一个抽象方法,没有抽象方法或者有多个抽象方法的接口都不是函数式接口。
必须是抽象方法,如果接口里面只有一个静态方法或者default方法,都不是函数式接口。
如果接口里面只有一个抽象方法,但是这个抽象方方法是Object类中的方法,如hashCode方法,这个接口也不是函数式接 口。
4、使用Lambda表达式实现函数式接口
public class testlambda {
public static void main(String[] args) {
ILove love = (int a)->{
System.out.println("i love number"+a);
};
love.love(14);
}
}
interface ILove{
void love(int a);
}
//使用Lambda表达式实现函数式接口
public class testlambda2 {
public static void main(String[] args) {
//简化1
ILove love = null;
love = a -> {
System.out.println("i love "+a);
};
love.love(5);
//简化2
love = a -> System.out.println("i love aa"+a);
love.love(520);
}
}
interface ILove{
void love(int a);
}
五、线程的生命周期(状态)
线程生命周期的阶段 | 描述 |
---|---|
新建 | 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 |
就绪 | 处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 |
运行 | 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能 |
阻塞 | 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态 |
死亡 | 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束 |
六、线程的常用方法
- Thread.currentThead():获取当前线程对象
- getPriority():获取当前线程的优先级
- setPriority():设置当前线程的优先级(线程优先级高,被CPU调度的概率大,但不代表一定会运行,还有小概率运行优先级低的线程。)
- isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)
- join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。有可能被外界中断产生InterruptedException 中断异常。
- sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。
- yield():调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)
- interrupt():中断线程
- wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的
- notify():唤醒当前线程,进入运行状态。该方法要在同步方法或者同步代码块中才使用的
- notifyAll():唤醒所有等待的线程。该方法要在同步方法或者同步代码块中才使用的
七、线程同步
多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
通过同步机制,来解决线程的安全问题。
1、同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
synchronized(object){
}
代码体现
public class SyncThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
new Thread(mr).start();
new Thread(mr).start();
}
}
class MyRunnable implements Runnable {
private int tickts = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (tickts > 0) {
// 方法一:使用同步代码块
synchronized (this) {
System.out.println("第" + (tickts--) + "张票售出");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2、同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public synchronized void save(){}
//synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
代码体现
public class SyncThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
new Thread(mr).start();
new Thread(mr).start();
}
}
class MyRunnable implements Runnable {
private int tickts = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 方法二:使用同步方法达到线程安全
saleTickts();
}
}
// 同步方法:同步的是当前对象
private synchronized void saleTickts() {
if (tickts > 0) {
System.out.println("第" + (tickts--) + "张票售出");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3、Lock锁
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
代码体现
public class SyncThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
new Thread(mr).start();
new Thread(mr).start();
}
}
class MyRunnable implements Runnable {
private int tickts = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 方法三:使用ReentrantLock,更加灵活,可以加入判断,在特定的情况下释放锁
saleTickts2();
}
}
ReentrantLock lock = new ReentrantLock();
// 方法三:使用ReentrantLock,更加灵活,可以加入判断,在特定的情况下释放锁
private void saleTickts2() {
lock.lock();
try {
if (tickts > 0) {
System.out.println("第" + (tickts--) + "张票售出");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 保证锁一定会被释放,不会出现死锁情况
} finally {
lock.unlock();// 释放锁
}
}
}
4、三种比较
优先级 Lock > 同步代码块 > 同步方法
- 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
- 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可
- 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
5、死锁
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁产生的必要条件:
(1)互斥条件:在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
产生死锁必须同时满足以上四个条件,只要其中任一条件不成立,死锁就不会发生。
我们只需要破坏其中的一个或多个条件就可以避免死锁发生
八、线程通信
通信方式
- wait():先释放资源,并开始等待
- wait(long timeout):释放资源等待timeout秒
- notify():唤醒一个处于等待状态的线程
- notifyAll():唤醒同一个对象上所有调用了wait()方法的线程,优先级高的优先调度
- 缓冲区(消息队列)
- 标志位:红绿灯
九、线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
-
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多长时间后会终止
代码体现
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
ExecutorService service= Executors.newFixedThreadPool(10);
//newFixedThreadPool 参数为:线程池大小
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}