一、进程和线程
像这样每一个应用就可以理解为一个进程。
在一个进程里可能会有多条逻辑同时进行,以保证满足这个进程需要完成的任务。那么这些不同的逻辑就是多个线程。
可以看到线程数是远大于进程的,说明每个进程里都有一个或多个线程。
二、线程创建的方式
1、Thread
创建线程方式1:继承Thread类,重写run()方法,调用start()开启线程
public class TestThread1 extends Thread {
@Override
public void run() {
//run()方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("run--" + i);
}
}
public static void main(String[] args) {
//创建线程对象
TestThread1 testThread1 = new TestThread1();
//调用start()方法,开启线程
testThread1.start();
//main()线程
for (int i = 0; i < 1000; i++) {
System.out.println("main--" + i);
}
}
}
2、Runnable
创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable的实现类,调用start方法
public class TestRunnable1 implements Runnable{
@Override
public void run() {
//run()方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("run--" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
TestRunnable1 testRunnable1 = new TestRunnable1();
//创建线程对象,通过线程对象开启线程,代理Proxy
new Thread(testRunnable1).start();
//main()线程
for (int i = 0; i < 1000; i++) {
System.out.println("main--" + i);
}
}
}
由于Runable接口是函数式接口,所以可以用lambda表达式简化。
public class DemoRunable implements Runnable{
@Override
public void run() {}
public static void main(String[] args) {
Runnable r = new DemoRunable();
r = ()->{ for(int i=0;i<10;i++){ System.out.println("run--"+i); } };
new Thread(r).start();
}
}
三、静态代理
静态代理模式
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
代理的好处:可以做真实对象额外的事情
这里以结婚作为例子,自己结婚只会输出"要结婚了,超开心",使用代理会输出"结婚之前,布置现场"“要结婚了,超开心”“结婚之后,收尾款”。代理就相当于婚庆公司的作用。
public class StaticProxy {
public static void main(String[] args) {
//自己结婚
System.out.println("--不使用Proxy--");
You you = new You();
you.HappyMarry();
System.out.println("\n\n\n");//分割
//婚庆公司代理
System.out.println("--使用Proxy--");
WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
//真实角色
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("要结婚了,超开心");
}
}
//代理角色
class WeddingCompany implements Marry{
private Marry targrt;
public WeddingCompany(Marry targrt) {
this.targrt = targrt;
}
@Override
public void HappyMarry() {
before();
this.targrt.HappyMarry();//真实对象结婚
after();
}
private void after() {
System.out.println("结婚之后,收尾款");
}
private void before() {
System.out.println("结婚之前,布置现场");
}
}
四、Lambda表达式
避免内部类定义过多降低代码的可读性。
lambda表达式的产生是一个逐步简化的过程,一个函数式接口调用的简化过程可以大致记作:1、使用实现类调用 2、使用静态内部类调用 3、使用局部内部类调用 4、使用匿名内部类调用 5、使用lambda表达式调用
下面放代码感受一下逐步简化的过程:
public class TestLambda1 {
//3.静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda2");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
like = new Like2();
like.lambda();
//4.局部内部类
class Like3 implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda3");
}
}
like = new Like3();
like.lambda();
//5.匿名内部类
like = new ILike() {
@Override
public void lambda() {
System.out.println("I like lambda4");
}
};
like.lambda();
//6.用lambda简化
like = ()->{
System.out.println("I like lambda5");
};
like.lambda();
}
}
//1.定义一个函数式接口
interface ILike{
void lambda();
}
//2.实现类
class Like implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda");
}
}
五、线程的状态
1、新建(NEW)
一个线程对象被new出来了
2、可运行(RUNNABLE)
线程对象的start()方法被调用,但是还没被cpu执行
3、运行(RUNNING)
线程对象被cpu执行着
4、阻塞(BLOCKED)
线程在运行状态下,由于某种原因又回到了可运行状态
5、死亡(DEAD)
线程执行完,或因为某种原因退出了run()方法,无法再次运行了
六、常用方法
方法 | 用处 |
---|---|
sleep(long millis) | 线程休眠一定的毫秒 |
join() | 所有线程阻塞,让调用该方法的线程先执行完 |
yield() | 线程退出cpu的占用,让cpu重新选择调用(相当于两个线程又回到同一起跑线) |
isAlive() | 判断线程是否还活着 |
start() | 启用线程 |
setPriority(int newPriority) | 设置某个线程的优先级1~10 |
interrupt() | 给线程一个中断的标记,并不会真的中断线程 |
七、线程同步
线程同步就是让线程按照预定的先后顺序运行
1、synchronized
先看一个在线程里使用List的例子
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}
由于未实现线程同步,导致这个list在执行一次add()方法的同时,有可能又被再次调用,而再次调用的时候,因为list已经被占用了,所以这个再次调用就是无效的。
这样,最后的输出结果就不是10000。
现在给list对象上个线程锁:
public class SafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
这样,list在线程里就是个安全的List了,只有上一次调用完成,才会进行下一次调用。输出的结果一定是10000。
类似的,synchronized
关键字不仅可以给对象上锁,还可以给方法上锁。
用一个买票的过程做示例:
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"苦逼的我").start();
new Thread(station,"牛逼的你").start();
new Thread(station,"可恶的黄牛").start();
}
}
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
buy();
}
}
private void buy() {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "买到" + ticketNums--);
}
}
买票的方法没有实现线程同步就会产生类似这样的结果:
因为前一个人买票、剩余票-1张的过程还没有结束,另一个人就开始买票了,这时候他们看到的票的张数是一样的,他们就会买到同一张票。而实际生活中,两个人买票一定是会有一个先后买票顺序的。所以将买票的方法上锁,即可实现线程同步:
public class SafeBuyTicket {
public static void main(String[] args) {
BuyTicket2 station = new BuyTicket2();
new Thread(station,"苦逼的我").start();
new Thread(station,"牛逼的你").start();
new Thread(station,"可恶的黄牛").start();
}
}
class BuyTicket2 implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
buy();
}
}
//同步方法(上锁,锁的是this)
private synchronized void buy() {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
//买票
System.out.println(Thread.currentThread().getName() + "买到" + ticketNums--);
}
}
输出结果类似于:
死锁(deadlock)
进程a和进程b都上了锁处于阻塞状态,这时a需要拿到b的锁才可以解除阻塞状态,b需要拿到a的锁才可以解除阻塞状态,这样的情况就叫死锁。
在编程的过程中一定要避免这样的情况。
2、Lock
优先级高
ReentrantLock类
使用示例:
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int tickNums = 10;
//定义Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();//加锁
if(tickNums>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tickNums--);
}else {
break;
}
} finally {
lock.unlock();
}
}
}
}
解释一下:
private final ReentrantLock lock = new ReentrantLock();
是利用ReentrantLock类定义了一个锁,这个锁是通过Java代码实现的,与synchronized不同,synchronized是提交给JVM实现的。
在lock.lock()和lock.unlock()之间的部分是不可以同时被多个线程调用的,如果没有这个锁,三个线程会同时进行tickNums- -,那么tickNums三个三个的减就会出现0和-1被输出出来。
加了锁的结果是:10 9 8 7 6 5 4 3 2 1
不加锁的结果是:10 9 8 7 6 5 4 3 2 1 0 -1(结果是三个三个出来的)
八、线程通信
wait()
使线程进入阻塞
notify()
唤醒线程
notifyAll()
唤醒所有线程
1、管程法(利用缓冲区)
举个例子:
一个生产鸡肉餐厅有厨师和顾客,这是两个毫不相关的人(线程),人专门吃鸡,厨师专门做鸡。那如何知道是否需要做鸡(调用厨师线程)或者有鸡可吃(调用顾客线程)呢?我们可以设置一个缓冲区——前台。当一只鸡都没有的时候,让顾客等着(阻塞),当鸡放不下的时候,让厨师等着(阻塞)。一旦厨师做了鸡,就通知顾客有鸡可吃(唤醒)。一旦顾客吃了鸡,就通知厨师做鸡(唤醒)。当然,吃鸡和做鸡的效率是由cpu选择的,吃鸡的选多了便会产生无鸡可吃的情况,反之亦然。
下面放上源码理解一下:
public class PC_buffer {
public static void main(String[] args) {
SynBuffer buffer = new SynBuffer();
new Productor(buffer).start();
new Consumer(buffer).start();
}
}
class Productor extends Thread {
SynBuffer buffer;
public Productor(SynBuffer buffer) {
this.buffer = buffer;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
buffer.push(new Chicken(i));
System.out.println("生产了" + i + "只鸡");
}
}
}
class Consumer extends Thread {
SynBuffer buffer;
public Consumer(SynBuffer buffer) {
this.buffer = buffer;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了编号为" + buffer.pop().id + "的鸡");
}
}
}
class Chicken {
int id;//产品编号
public Chicken(int id) {
this.id = id;
}
}
class SynBuffer {
//需要一个容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken) {
//如果容器满了,就需要等待消费者消费
if (count == chickens.length) {
//通知消费者消费,生产者等待
try {
this.wait();//释放锁,等待进程
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果未满,就需要丢入产品
chickens[count] = chicken;
count++;
//可以通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() {
//判断能否消费
if (count == 0) {
//消费者等待生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
Chicken chicken = chickens[count];
//通知生产者生产
this.notifyAll();
return chicken;
}
}
2、信号灯法(利用标志位)
将管程法的缓冲区变成了一个标志位。
举个例子:演员表演节目的电视上。一表演,标志位就变成了可观看,就会通知观众观看(唤醒),如果观众还没观看,演员就会等待(阻塞)。观众看了节目后,标志位又会变为不可观看,通知演员表演(唤醒),观众等待(阻塞)。
上源码理解一下:
public class PC_flag {
public static void main(String[] args) {
TV tv = new TV();
new viewer(tv).start();
new actor(tv).start();
}
}
//生产者-->演员
class actor extends Thread {
TV tv;
public actor(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i%2==0){
this.tv.perform("斗罗大陆");
}else {
this.tv.perform("广告");
}
}
}
}
//消费者-->观众
class viewer extends Thread {
TV tv;
public viewer(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV {
//演员表演,观众等待 T
//观众观看,演员等待 F
String voice;//节目
boolean flag = true;
//表演
public synchronized void perform(String voice) {
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
//通知观众观看
this.notifyAll();//通知
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + voice);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
九、线程池
(线程池我只会简单的调用,相关知识需要自行搜索)
线程池的简单使用示例:
//1.创建服务,创建线程池,参数为池子大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行execute
service.execute(new MyThread());
service.execute(new MyThread());
//2.关闭连接
service.shutdown();
线程池的几个参数:
- 池的大小
- 最大连接数
- 连接保持时间(一定时间某个线程连接了并未操作就自动断开)