线程创建有4种方式 线程同步有3种方式
程序、进程、线程
- 一个java应用程序至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程(main执行的同时,有失效的局部变量会被垃圾回收线程回收)
- 并行与并发
并行:多个CPU同时处理多个任务
并发:一个CPU同时(划分时间片)处理多个任务
线程的创建和使用(四种之前两种)
1️⃣继承java.lang.Thread类
- 方式一:继承于Thread类,重写run方法,将此线程应该做的事情写在run的方法体里,创建Thread类的子类对象,调用start()
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<101;i++){
System.out.println(i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread myThread=new MyThread();
//start:启动当前线程,调用当前线程的run方法
myThread.start();
for(int i=0;i<132;i++){
System.out.println("hello");
}
}
}
- 方式二:创建Thread类的匿名子类(只用一次)
public class ThreadTest2 {
//两个线程,一个输出奇数,一个输出偶数
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2==0){
System.out.println(i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2!=0){
System.out.println(i);
}
}
}
}.start();
}
}
Thread类的常用方法
- void start() 启动当前线程,调用当前线程的run方法
- run() 需要重写,
- String getName() 获取当前线程的名字
- void setName(String name) 设置当前线程的名字,也可以通过构造器来设置
- static Thread currentThread() 返回执行当前代码的线程
- yield() 释放当前CPU的执行权,可能被其他线程抢占,但是也可能仍被当前线程抢占
public class ThreadMethod {
public static void main(String[] args) {
HelloThread helloThread=new HelloThread("线程一");
//helloThread.setName("线程一");
helloThread.start();
//给主线程命名
Thread.currentThread().setName("主线程");
for(int i=0;i<=100;i++){
if(i%2!=0)
System.out.println(Thread.currentThread().getName()+":"+i);
//这里也加上更容易看出效果
if(i%20==0){
Thread.currentThread().yield();
}
}
}
}
class HelloThread extends Thread{
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
//释放当前CPU的执行权
if(i%20==0){
yield();
}
}
}
public HelloThread() {
}
public HelloThread(String name) {
super(name);
}
}
- join(): 在线程A中调用B的join方法,线程A进入阻塞状态,直到线程B完全执行完之后,线程A才结束阻塞状态,等待CPU分配资源继续执行
场景:当前线程需要另外一个线程执行完之后的数据,所以对停下来,等待另一个线程执行完之后,再继续执行
public static void main(String[] args) {
HelloThread helloThread=new HelloThread();
helloThread.start();
for(int i=0;i<=100;i++){
if(i%2!=0)
System.out.println(Thread.currentThread().getName()+":"+i);
//join
if(i==19) {
try {
helloThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- stop()强制结束线程的生命周期,不推荐使用
- sleep(long millitime) 毫秒,将当前线程阻塞指定时间,然后恢复,等待CPU分配资源
注意问题:
1> 会抛出异常,但是只能使用try-catch的方式,不能使用throws,因为run方法是继承来的,父类的run方法没有抛出异常,所以子类重写的run也不能抛出异常
2>是一个静态方法,可以直接Thread.sleep(i)
应用:倒计时 - isAlive() 判断当前线程是否存活
2️⃣实现Runnable接口
- 方式:创建一个实现了Runnable接口的实现类,实现接口的run方法;创建实现类的对象;将此对象作为参数,传递到Thread类的构造器中,创建Thread类的对象;通过Thread类的对象调用start()
public class ThreadTestInterface {
public static void main(String[] args) {
MyRun myRun=new MyRun();
//start()①启动线程 ②调用当前线程的run方法
/*
*源码:
* private Runnable target;
* public void run(){
* if(target!=null){
* target.run();
* }
* }
*
*/
new Thread(myRun).start();
}
}
class MyRun implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
🤨两种方法的比较
开发中优先选择实现Runnable接口的方式:
①实现的方式没有类的单继承的局限性
②实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread类本身也实现了Runnable接口,所以相同点是两种方法都需要重写run方法,将线程需要执行的逻辑声明在run()中
线程的调度(优先级)
- CPU的调度策略:时间片,抢占式(高优先级的线程抢占CPU)
- Java调度方法:
1>同优先级组成先进先出队列,使用时间片策略
2>高优先级使用优先调度的抢占策略 - 线程优先级:static final
MAX_PRIORITY: 10 (先被执行的概率高,而不是一定会先被全部执行完)
MIIN_PRIORITY:1
NORM_PRIORITY:5 (默认情况下) - getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级,start之前设置线程的优先级
线程的生命周期
Thread.state类记录了线程的五种状态
- 新建:Thread类或子类的对象被声明并且创建时,新生的线程对象处于新建状态
- 就绪:被start后,进入线程队列等待CPU时间片,此时已经具备了运行的条件,只是没有分配到CPU资源
- 运行:当就绪的线程被调度获得CPU资源时,进入运行状态,run方法线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行
-
死亡:线程完成了全部工作或线程被提前强制性地中止,或出现异常导致结束
线程的同步
多个线程操作共享数据的时候,可能会产生线程安全问题,通过同步机制解决线程安全问题
同步的方式,解决了线程安全的问题,但操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程过程,效率会低一点
🧡方法一:同步代码块
关键字:synchronized(同步监视器){需要被同步的代码}
1>需要被同步的代码:操作共享数据的代码,注意不能包含代码少了(用到了共享数据必须包进去),也不能包含代码多了(变成单线程事小,导致代码逻辑错误事大)
2>共享数据:多个线程共同操作的变量
3>同步监视器:俗称,锁。任何一个类的对象,都可以充当锁,但❗要求多个线程必须要共用同一把锁
- 实现Runnable的方法
/**
* @Author: ssy
* @Description: 创建三个窗口买票。总票数100。解决线程同步问题
* @Date: Created in 14:51 2020/11/11
* @Modified By:
*/
public class WindowTest {
public static void main(String[] args) {
Window window1=new Window();
Thread thread1=new Thread(window1);
Thread thread2=new Thread(window1);
Thread thread3=new Thread(window1);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();;
thread2.start();
thread3.start();
//输出乱序是因为输出到控制台上需要时间,实际上票是按照顺序产生的
}
}
class Window implements Runnable{
private int ticket=100; //三个窗口一共只有100张票
Object object=new Object(); //同步监视器
@Override
public void run() {
while (true){
synchronized(object){ //包多了导致程序结果出错的情况:把while(true)也包进去了
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口买票:票号为"+ticket);
ticket--;
}
else break;
}
}
}
}
- 继承Thread的方法
/**
* @Author: ssy
* @Description: 买票,三个窗口一共卖100张票,一定要注意同步监视器需要使用的是同一个对象
* @Date: Created in 9:55 2020/11/12
* @Modified By:
*/
public class WindowTest2 {
public static void main(String[] args) {
Window2 thread1=new Window2();
Window2 thread2=new Window2();
Window2 thread3=new Window2();
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();;
thread2.start();
thread3.start();
//输出乱序是因为输出到控制台上需要时间,实际上票是按照顺序产生的
}
}
class Window2 extends Thread{
private static int ticket=100; //三个窗口一共只有100张票
static Object object=new Object(); //同步监视器 static 静态变量
@Override
public void run() {
while (true){
synchronized(object){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口买票:票号为"+ticket);
ticket--;
}
else break;
}
}
}
}
- 实现Runnable方法的简化(应该时刻注意,锁应该是共用的同一个)
synchronized(this){ //this是当前对象,对于实现Runnable的方法来说,this是多线程的同一个对象
//同步代码块
}
- 继承Thread方法的简化(反射相关知识,类也是对象,类只会加载一次,所以可以作为锁使用)
synchronized(Window2.class){ //Window2是继承了Thread类的子类,见上面的抢票代码
}
🧡方法二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的。
1>同步方法仍然涉及到同步监视器,只是不需要显式声明,使用的是默认的
2>非静态的同步方法(Runnable),默认的同步监视器是this
3>静态的同步方法(Thread),默认的同步监视器是:当前类本身
/**
* @Author: ssy
* @Description: 使用同步方法解决实现Runnable的同步问题
* @Date: Created in 10:22 2020/11/12
* @Modified By:
*/
public class WindowTest3 {
public static void main(String[] args) {
Window3 window1= new Window3();
Thread thread1=new Thread(window1);
Thread thread2=new Thread(window1);
Thread thread3=new Thread(window1);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();;
thread2.start();
thread3.start();
//输出乱序是因为输出到控制台上需要时间,实际上票是按照顺序产生的
}
}
class Window3 implements Runnable{
Object object=new Object(); //同步监视器
private int ticket=100; //三个窗口一共只有100张票
@Override
public void run() {
while (true){
if(saleTicket()==0) break; //因为有while的存在,不能直接在run方法上加synchronized
//同样因为while的存在,要注意如何提出方法,是否需要返回值,能够使循环结束
}
}
private synchronized int saleTicket(){ //默认同步监视器就是 this (继承Thread的方法慎用,应该加上static,同步监视器xxxx.class)
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口买票:票号为"+ticket);
ticket--;
return 1;
}
else return 0;
}
}
- 单例懒汉模式的线程同步
/**
* @Author: ssy
* @Description: 单例 懒汉 高校模式
* @Date: Created in 11:24 2020/11/12
* @Modified By:
*/
public class BankTest {
}
class Bank{
private Bank(){}
private static Bank instance=null;
public static Bank getInstance(){
//方式一
/*
*
* synchronized (Bank.class){
if(instance==null)
instance=new Bank();
return instance;
}
*
*/
//方式二: 高效
if(instance==null){ //不需要每次都走一遍synchronized
synchronized (Bank.class){
if(instance==null)
instance=new Bank();
}
}
return instance;
}
}
🧡方法三:Lock锁(JDK5.0新增)
- 从JDK5.0开始,java提供了更强大的线程同步机制,通过显示定义同步锁对象来实现同步。同步锁使用lock锁实现
- java.util.concurrent.locks.Lock接口
- ReentrantLock实现了Lock接口,拥有与synchronized相同的并发性和内存语义。需要显式地加锁,释放锁
ReentrantLock lock = new ReentrantLock();//参数fair,无参方式默认为false;显式设置为true,则线程遵守先进先出规则
lock.lock();
/*
同步代码块,lock锁住,保证同步代码在锁住的期间是一个单线程
*/
lock.unlock();
- 注意问题:synchronized需要注意使用的是不是一个同步监视器,同样,lock需要注意使用的是否为同一把锁
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: ssy
* @Description: 三个窗口同时卖票 多线程 线程安全 lock锁
* @Date: Created in 17:22 2020/11/12
* @Modified By:
*/
public class WindowTest4 {
public static void main(String[] args) {
Window4 window=new Window4();
Thread thread1=new Thread(window);
Thread thread2=new Thread(window);
Thread thread3=new Thread(window);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class Window4 implements Runnable{
private ReentrantLock lock=new ReentrantLock();
private int ticket=100;
@Override
public void run() {
while (true){
try{
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":票号"+ticket);
ticket--;
}
else break;
}finally {
lock.unlock();
}
}
}
}
- 面试题1:如何解决线程安全问题?有几种方式?
答:如上 - 面试题2:synchronized和lock的异同点?
相同点:都可以用来解决线程安全问题。
不同:①Lock是显式锁,需要手动启动(lock()),手动结束(unlock()),synchronized是隐式锁,执行完相应的同步代码之后,自动释放同步监视器。
②Lock只有代码块锁,synchronized有代码块锁和方法锁。
③使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。 - 优先使用顺序:Lock -> 同步代码块 ->同步方法
线程的通信
- 不同的线程操作共享数据,有一些“交流”,都可以看作是线程的通信
- import java.lang.Object
- wait():使调用该方法的线程进入阻塞状态,并释放同步监视器
- notify():唤醒一个wait的线程。如果多个线程被wait,则唤醒优先级高的
- notifyAll():唤醒所有的wait的线程
- 注意的问题:上述三个方法只能用在同步代码块或者同步方法(synchronized)里,不能使用在Lock锁中。三个方法的调用者是同步监视器
- 例题:使用两个线程打印1-100,线程1、线程2交替打印
public class NumberTest {
public static void main(String[] args) {
Number number=new Number();
Thread thread1=new Thread(number);
Thread thread2=new Thread(number);
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while(true){
synchronized (this){
notify(); //唤醒一个被wait的线程
if(number<=100){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else break;
}
}
}
}
- wait和sleep方法的异同点
相同点:一旦执行,都能使当前线程进入阻塞状态
不同点:
1>两个方法声明的位置不同,Thread类中声明的sleep方法,Object类中声明的wait方法
2>调用的范围不同:sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3>关于是否释放同步监视器:如果两个方法都是用在同步代码块或同步方法中,sleep不会释放同步监视器,wait方法会释放同步监视器 - 经典问题:生产者/消费者问题
/**
* @Author: ssy
* @Description: 生产者消费者模式 多线程 线程安全 线程通信
* @Date: Created in 16:40 2020/11/13
* @Modified By:
*/
public class ProductTest {
public static void main(String[] args) {
Product product=new Product();
Producer producer=new Producer(product);
Customer customer=new Customer(product);
producer.setName("生产者");
customer.setName("消费者");
producer.start();
customer.start();
}
}
class Product{
private int account=0;
public synchronized void produceProduct(){
if(account<20){
account++;
System.out.println(Thread.currentThread().getName()+"生产商品号:"+account);
notify();
}
else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void saleProduct(){
if(account>0){
System.out.println(Thread.currentThread().getName()+"消费商品号:"+account);
account--;
notify();
}
else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
private Product product;
public Producer(Product product){
this.product=product;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.produceProduct();
}
}
}
class Customer extends Thread{
private Product product;
public Customer(Product product){
this.product=product;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.saleProduct();
}
}
}
线程的死锁问题
- 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁,出现死锁后,不会出现异常,不会出现提示,只是线程都处于阻塞状态,无法继续
- 解决的方法:专门的算法、原则;尽量减少同步资源的定义;尽量避免嵌套同步
线程的创建和使用(续--后四种--JDK5.0新增线程创建方式)
3️⃣继承Callable接口
- 与继承Runnable接口相比,Callable接口的更能更强大
①有返回值
②可以抛出异常(throws)
③支持泛型的返回值
④需要借助FutureTask类,比如获取返回结果 - Future接口
①可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
②FutureTask是Future接口的唯一的实现类
③FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable接口被线程执行,又可以作为Future接口得到Callable的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author: ssy
* @Description:
* @Date: Created in 9:30 2020/11/16
* @Modified By:
*/
public class ThreadTest3 {
public static void main(String[] args) {
MyCallable myCallable=new MyCallable();
FutureTask futureTask=new FutureTask(myCallable);
Thread thread=new Thread(futureTask);
thread.setName("当前线程");
thread.start();
//获取线程的返回值
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
for(int i=0;i<100;i++){
if(i%2==0)
System.out.println(Thread.currentThread().getName()+": "+i);
}
return 100;//测试return值
}
}
4️⃣使用线程池💢
- 线程池适用于经常创建和销毁、使用量特别大的资源、比如并发情况下的线程,对性能影响很大。提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:提高响应速度(减少了创建新线程的时间)、降低资源消耗(重复使用线程池中的线程,不需要每次都创建)、便于线程管理(corePoolSize、maximumPoolSize、keepAliveTime.......几个属性的搭配设置很重要,但我还只是学基础,就还没有看)
- 线程池相关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.newSingleThreadPool() 创建一个只有一个线程的线程池
④Executors.newScheduledThreadPool(n) 创建一个线程池,它可以安排在给定延迟后运行命令或者定期地执行
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @Author: ssy
* @Description:
* @Date: Created in 9:58 2020/11/16
* @Modified By:
*/
public class ThreadTest4 {
public static void main(String[] args) {
//工厂方法创建不同类型的线程池,利用多态性使用接口来承载
ExecutorService poolExecutor= Executors.newFixedThreadPool(10);
//需要对属性进行设置的时候,需要强转为相应类的对象,因为接口中定义的属性为常量,不能用接口来改变属性
ThreadPoolExecutor pool=(ThreadPoolExecutor)poolExecutor;
pool.setCorePoolSize(15);
//pool.setKeepAliveTime(100,xx);
//使用接口来调用函数,执行指定的线程的操作。需要提供实现Runnable或者Callable接口实现类的对象
poolExecutor.execute(new MyRunnable());
poolExecutor.submit(new MyCallable1());
//不适用的时候要关闭连接池
poolExecutor.shutdown();
}
}
class MyRunnable 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 MyCallable1 implements Callable{
@Override
public Object call() throws Exception {
for(int i=0;i<100;i++){
if(i%2==1)
System.out.println(Thread.currentThread().getName()+": "+i);
}
return null; //没有返回值就return null
}
}