线程
创建线程的方式
-
继承Thread类,重写run方法
public class demo extends Thread{ @Override public void run() { //写线程操作的代码 } } //匿名方式创建 new Thread() { @Override public void run() { //写线程操作的代码 } }.start();
-
实现Runable接口,实现run方法
public class demo implement Runable{ @Override public void run() { //写线程操作的代码 } } //匿名方式创建 demo demo = new demo(); new Thread(demo) { @Override public void run() { //写线程操作的代码 } }.start();
-
实现Callable接口,实现call方法
-
使用Exector的工具类去创建线程池,通过线程池获取线程
几个创建线程的区别
-
java是单继承模式,接口可以实现多个接口,实现接口还可以继承类。推荐使用接口方式,使系统扩展更灵活。
-
实现Callable接口创建线程和实现Runable接口创建线程基本一样,区别在于call方法可以有返回值。
-
使用Callable接口或者Runable接口,比较容易实现多个线程共享一个Tagert对象,非常适合实现多线程处理同一份业务,
形成清晰的代码逻辑,体现面向对象的思想。
-
线程实现Runable接口或者Callable接口,如果要访问当前线程,必须调用Thread.currentThread()方法,但是继承Thread类,
可以使用this关键字实现对应的效果。
-
使用Callable或者Thread或者Runable实现线程的创建,如果频繁创建会消耗系统资源,影响性能,而使用线程池创建不适应线程
的时候可以放回线程池,用的时候再从线程池中取,目前在项目之中使用线程池。
start方法和run方法的区别
- start方法时开启进程,调用后线程会放到等待队列,等待JVM调度。等待CPU时间片再去执行
- run方法用来封装子线程需要执行的业务代码,run方法执行结束,线程终止。
线程的状态:
- NEW :创建一个新的线程
- RUNABLE:线程运行状态
- WAITING:线程休眠状态
- TIME-WAITING:线程永久休眠
- TERMIMATED:线程死亡状态
- BLOCKED:线程为抢断到CPU资源,阻塞状态
new方法创建线程,调用start方法,线程去抢夺CPU资源,如果得到CPU时间片,处于Runable状态,没有则进入blocked阻塞状态;运行状态可以和阻塞状态进行互换,就是抢夺CPU资源。线程执行完run方法之后,线程结束,进入死亡状态;如果线程在运行状态被停止(wait方法),则进入等待状态,两者互换是用Object.wait方法和Object.notify方法;当然线程处于等待状态时候等待时间结束,就可以进入与阻塞状态进行与其他线抢夺CPU资源。
线程安全
使用线程的情况下必须考虑线程安全的情况,否则会导致数据的错误,等待或者其他的严重的错误。
在不进行安全设置之前,示例一个如下的代码:
测试一个多线程进行买票的操作:
public class sale implements Runnable{
private int ticketNums = 6;
public static void main(String[] args) {
sale s = new sale();//一个共享资源
//创建三个线程 使用同一个资源
new Thread(s,"线程一").start();
new Thread(s,"线程二").start();
new Thread(s,"线程三").start();
}
@Override
public void run() {
while(true) {
if(ticketNums<0) {
break;
}
try {
Thread.sleep(200);//为了体现出错误的代码
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"->>"+ticketNums--);
}
}
}
//结果 当然每次运行的结果不同 因为三个线程对资源的抢夺说不定
线程三->>6
线程二->>5
线程一->>5
线程三->>4
线程二->>2
线程一->>3
线程一->>1
线程三->>0
线程二->>-1
结果出现了错误数据(0和-1),和重复数据(5)
错误数据是因为当还有一张票的时候,如果线程一抢夺到资源,进入if语句,进入睡眠状态,失去CPU的执行权;此时线程二抢夺到资源,进入if语句,进入睡眠状态,失去CPU的执行权;此时线程二抢夺到资源,进入if语句,进入睡眠状态,失去CPU的执行权。此时,如果这三个线程逐次睡醒,则对于这个一个票进行减少,则会出来不正确的数据。
重复数据是因为当三个线程同时进行处理买票,则会出现相同数据。
处理线程安全的三种方式
-
synchronized代码块
使用锁对象进行资源限制 锁对象声明到外面,否则每个线程都会创建一个锁对象
Object obj = new Objcet(); synchronized(obj){ //操作共享资源的代码 }
-
synchronized方法
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
public synchronized void test() { if (ticketNums <= 0) { flag = false; return; } try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "->>" + ticketNums--); }
-
使用Lock锁
java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock()获取锁。
void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁public void run() { //在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁 l.lock(); if (ticketNums <= 0) { flag = false; return; } try { Thread.sleep(200); System.out.println(Thread.currentThread().getName() + "->>" + ticketNums--); } catch (InterruptedException e) { e.printStackTrace(); } finally { //在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁 l.unlock();//无论程序是否异常,都会把锁释放 } } }
注意
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法
Obejct类中的方法
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify()
唤醒在此对象监视器上等待的单个线程。
会继续执行wait方法之后的代码
进入到TimeWaiting(计时等待(毫秒))有两种方式
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
线程通信
等待与唤醒机制:有效的利用资源
使用资源的状态进行判断
notify()方法和 notifyAll()方法和wait()方法
唤醒单个线程和唤醒全部线程和等待的线程
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继 承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
//产品
public class goods {
private String a;
private String b;
private Boolean status = false;
public String getA() {
return a;
}
public Boolean getStatus() {
return status;
}
public String getB() {
return b;
}
public void setPi(String a) {
this.a = a;
}
public void setStatus(Boolean status) {
this.status = status;
}
public void setXian(String b) {
this.b = b;
}
}
//消费者
public class buyer extends Thread{
private goods goods;
public buyer(goods goods){
this.goods=goods;
}
public buyer(){
}
@Override
public void run() {
while(true) {
synchronized (goods) {
if(goods.getStatus()) {
System.out.println("买货物"+goods.getA()+goods.getB()+"两秒");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.setStatus(false);
System.out.println("买完了"+goods.getA()+goods.getB());
goods.notify();
}else {
try {
goods.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//生产者
public class shop extends Thread{
private goods goods;
public shop(goods goods){
this.goods=goods;
}
public shop(){
}
@Override
public void run() {
while(true) {
synchronized (goods) {
if(!goods.getStatus()) {
try {
goods.setA("1one");
goods.setB("2one3one");
System.out.println("生产货物中3秒"+goods.getA()+goods.getB());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.setStatus(true);
System.out.println("好了"+goods.getA()+baozi.getB());
goods.notify();
}else {
try {
goods.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//测试类
public class test {
public static void main(String[] args) {
goods goods = new goods();
shop shop = new shop(goods);
buyer buyer = new buyer(goods);
shop.start();
buyer.start();
}
}
线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池就是一个容纳很多线程的容器,一般有HashMap,LinkedList,ArrayList,HashSet
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内 存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用:
java的线程池顶级接口是import java.util.concurrent包的Executors接口 ,可以使用newFixedThreadPool方法去创建线程池可以自定义线程数量。返回类型是ExecutorService接口;(体现的是面向接口编程)
创建一个是实现了Runable接口的实现类
然后执行ExecutorService接口的submit方法传入实现类就可以
最后是ExecutorService接口的shutdown方法,这个是摧毁线程池的操作(不建议使用),线程池可以重复利用。
public class RunableImpl implements Runnable{
@Override
public void run() {
System.out.println("使用了线程池"+Thread.currentThread().getName());
}
}
// submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
public class pool {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(3);
es.submit(new RunableImpl());
es.submit(new RunableImpl());
es.submit(new RunableImpl());
es.submit(new RunableImpl());
es.shutdown();
}
}
//这里创建了三个线程大小的线程池,而有四个任务,当有归还线程的就会执行后面的任务
//使用了线程池pool-1-thread-1
//使用了线程池pool-1-thread-2
//使用了线程池pool-1-thread-3
//使用了线程池pool-1-thread-3
_Lambda表达式
标准格式
()->{}