程序:打的代码(静态的不可变的)
进程:是程序的一次执行过程,会给每个进程分配不同的内存,有生命周期,是动态的+
线程:程序内部的一条执行路径(一个任务)
单核cpu:cpu在执行任务的时候是按照时间片来执行的,一个时间片只能执行一个线程(任务),所以单核cpu是假的多线程
多核cpu:可以做到同一时间多个任务同时进行
并行和并发
并行:多个cpu同时执行多个任务
并发:一个cpu “同时” 执行多个任务(采用时间片切换)
之前学的是单线程吗?
不是,至少三个。并且线程之前会争抢资源
自制多线程(创建线程的三种方式)
实质上是创建一个线程类,线程对象才具备争抢资源的能力。创建线程类需要继承thread 类
想让线程做的事情要写进run方法中,调用线程对象时需要启动线程(调start()方法)。才能让线程可以争抢资源
1.继承thread类
设置和读取线程名字的方法
1.setname getname
thread.currentthread() 获取当前线程
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
class Test{
public static void main(String[] args) {
//获取当前线程并命名
Thread.currentThread().setName("主线程");
TestThread tt = new TestThread();
TestThread tt1 = new TestThread();
tt.setName("子线程1");
tt1.setName("子线程2");
tt1.start();
tt.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
2.通过构造器设置
抢票练习
多线程在有限资源情况下抢票。会发生抢到同一张票或签到-1张票
public class Train {
public static void main(String[] args) {
Testthread t1 = new Testthread("窗口1");
t1.start();
Testthread t2 = new Testthread("窗口2");
t2.start();
Testthread t3 = new Testthread("窗口3");
t3.start();
}
}
public class Testthread extends Thread{
static int tickets = 10;
public Testthread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(tickets>0)
System.out.println(this.getName()+"抢到了第"+ tickets-- +"张票");
}
}
}
创建线程的第二种方式(实现Runnable 接口)
实现Runnable 接口,实现runnable接口的类无法再调用Thread类中的start()方法,所以需要创建Thread对象,并把实现Runnable接口的线程对象当作参数传进Thread对象的构造方法中(此时创造的还是实现runnable接口的那个类的对象)
public class Test {
public static void main(String[] args) {
TnThread tt = new TnThread();
Thread t = new Thread(tt,"子线程");
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread()+"------"+i);
}
}
}
public class TnThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
抢火车票2
因为所有线程共享一个线程对象,该对象中的属性虽然没有被static修饰,仍是唯一的,因为自始至终只有一个对象。
public class Test {
public static void main(String[] args) {
//定义一个线程对象
TnThread tt = new TnThread();
Thread t = new Thread(tt,"子线程");
t.start();
Thread t1 = new Thread(tt,"子线程2");
t1.start();
Thread t2 = new Thread(tt,"子线程3");
t2.start();
}
}
public class TnThread implements Runnable{
int tickets = 10; //此时没有被static修饰,所有线程不共享此变量
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(tickets>0)
System.out.println(Thread.currentThread().getName()+"抢到了第"+ tickets-- +"张票");
}
}
}
两种创建线程方式(继承thread类 实现runnable接口)的联系(实际上是thread和runnable的关系)
第三种方式,实现callable接口
引入:无论是继承thread类还是实现runnable接口都需要重写run()方法,让线程做事情的逻辑代码也写在run方法中,然而这种方式有以下几点不足。1.不能有返回值 2.无法抛出异常
基于以上两种不足,jdk1.5以后出现了第三种创建线程的方式 实现callable接口
我们创建的线程就是callable接口的实现类,然而调用时发现并没有start方法,所以只能借助thread类对象调用start方法,而thread类没有可以传入Callable的构造器,但是有带runnable参数的构造器,所以需要借助futureTask类(该类实现了runnable接口)创建对象,再将这个对象传进thread构造器中
public class TestRandomNum implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);
}
}
class Test{
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestRandomNum trn = new TestRandomNum();
FutureTask<Integer> ft = new FutureTask(trn);
Thread th = new Thread(ft);
//开启random测试线程类
th.start();
//获取call中的返回值,通过FutureTask
//FutureTask 是泛型类,不指定泛型则通过get方法获取的返回值是object类型
Integer integer = ft.get();
System.out.println(integer);
}
}
线程的生命周期
新生(new Thread)
就绪(调用start()之后)
运行(获得cpu资源)——阻塞(运行过程中可能会卡住),阻塞结束后线程回到就绪状态等待cpu调用
死亡(执行完逻辑代码)
线程常用方法
线程的优先级
设置优先级的方法 setPriority()
public class TestThread01 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
class TestThread02 extends Thread{
@Override
public void run() {
for (int i = 40; i <= 50; i++) {
System.out.println(i);
}
}
}
class Test{
public static void main(String[] args) {
//创建两个子线程,并让他们争抢资源
TestThread01 tt1 = new TestThread01();
//执行之前先设置优先级
tt1.setPriority(1);
tt1.start();
TestThread02 tt2 = new TestThread02();
tt2.setPriority(10);
tt2.start();
}
}
常见方法
join 半路杀出个程咬金
主线程和子线程会争抢资源 (制造阻塞)
然而当一个子线程调用了join方法后,该子线程一定会先被执行完才执行其他线程,先start,再join
class Test{
public static void main(String[] args) throws InterruptedException {
TestThread01 tt1 = new TestThread01();
for (int i = 1; i < 100; i++) {
if(i==6) {
tt1.start();
tt1.join();
}
System.out.println("main-------"+i);
}
}
}
sleep()
thread的静态方法,人为的制造阻塞
让线程等待一段时间再执行,所以需要参数是一个毫秒数
打印秒数
class Test{
public static void main(String[] args) throws InterruptedException {
DateFormat df = new SimpleDateFormat("HH:mm:ss");
while (true) {
Date d = new Date();
Thread.sleep(1000);
System.out.println(df.format(d));
}
}
}
setDaemon(boolean) 设置伴随线程
当主线程消亡后,子线程也随之消亡(陪葬)
然而也不是立马就死,垂死挣扎一段时间后会消亡,所以可以看到有时候主线程结束后子线程还能执行一小会
class Test{
public static void main(String[] args) throws InterruptedException {
TestThread01 tt01 = new TestThread01();
//设置伴随线程
tt01.setDaemon(true);
tt01.start(); //打印0-999
for (int i = 0; i < 10; i++) {
System.out.println("main------"+i);
}
}
}
线程的同步
同步代码块
线程安全问题:
多个线程会争抢资源,有时会发生这个线程里的逻辑代码还没执行完就被另一个线程把资源抢走,这样结果会出现问题。(线程不安全),为了保证线程安全,我们选择给要执行的代码(逻辑代码)加锁(在使用资源的过程中加锁),保证程序执行完资源才释放。只锁具有安全隐患的代码块
public class Thread01 implements Runnable{
int tickets = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this){
if(tickets>0)
System.out.println(Thread.currentThread().getName()+"抢到了第"+tickets-- +"张票");
}
}
}
}
sychronized(锁子)
锁子必须唯一。否则依然会有线程安全问题
锁子(同步监视器)有两种状态 open/close ,初始状态是open,当有线程使用cpu资源时就转换为close。同步代码块执行完毕后释放锁,此时又转换为open状态。
如果有多个同步代码块,有的同步监视器被锁住,但不影响其他未被锁住的同步代码块的使用
共享数据:多个线程能够操作的数据
sychronized(同步监视器){
操纵共享数据的代码块;
}
锁子必须是引用数据类型
同步监视器总结:
总结1:认识同步监视器(锁子) ----- synchronized(同步监视器){ }
1)必须是引用数据类型,不能是基本数据类型
2)也可以创建一个专门的同步监视器,没有任何业务含义
3)一般使用共享资源做同步监视器即可
4)在同步代码块中不能改变同步监视器对象的引用
5)尽量不要String和包装类Integer做同步监视器
6)建议使用final修饰同步监视器
总结2:同步代码块的执行过程
1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
总结3:其他
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
同步方法
如果是继承了thread的线程类使用同步方法,依然无法保证线程安全,因为该线程类是可以new出多个对象,相当于拥有了多个锁。如果一定要使用同步方法,需要加上static 修饰(锁住的是字节码信息)。实现runnable接口的类可以不用加,因为始终使用的是一个线程对象。(不加锁的是对象)
public class Thread02 extends Thread{
static int tickets = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
buyTickets();
}
}
//锁修饰方法
private static synchronized void buyTickets() { ==锁住的 同步监视器 Thread02.class==
if(tickets>0)
System.out.println(Thread.currentThread().getName()+"抢到第"+tickets-- +"张票");
}
}
class Test02{
public static void main(String[] args) {
Thread02 t2 = new Thread02();
t2.start();
Thread02 tt = new Thread02();
tt.start();
}
}
总结
总结1:
多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。
咱们的锁一般都是引用数据类型的。
目的:解决了线程安全问题。
总结2:关于同步方法
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this
静态同步方法的同步监视器是 类名.class 字节码信息对象 - 同步代码块的效率要高于同步方法
原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部 - 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
LOCK锁
获取lock锁要在run方法外部,每个对象调用的方法都是初始化过的,会获得许多新的锁,不能达到线程安全的目标
public class Thread01 implements Runnable{
int tickets = 10;
Lock l = new ReentrantLock();//得到一把锁
@Override
public void run() {
for (int i = 0; i < 100; i++) {
l.lock(); //打开锁
// buytickets();
try {
if(tickets>0)
System.out.println(Thread.currentThread().getName()+"抢到了第"+tickets-- +"张票");
}catch (Exception e){
e.printStackTrace();
}finally {
l.unlock(); //关闭锁
}
}
}
Lock和synchronized的区别
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
线程通信
有时候需要线程有序使用资源,而不是谁抢到谁使用,所以需要线程之间进行通信,一个线程用完通知别的线程一声
生产者消费者
生产者生产一件,消费者购买一件
解决错乱和null值
之所以会出现错乱是因为一个线程还没执行利索另一个线程就把资源抢过去了,所以会造成打印错乱。
解决方式是加锁,但要保证生产者和消费者是同一把锁,如果将this当作锁,那么两个线程对象将分别拥有一把锁,依然不能保证线程执行安全,将商品对象当作锁就没有问题,首先商品对象是唯一的,谁拥有商品,另一个人就要等着拥有商品的人把事做完
同步代码块解决
商品类
public class Product {
private String brand;//品牌
private String name;//名称
}
生产者线程
public class ProducerThread extends Thread{
private Product p; //生产者线程生产商品
public ProducerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (p){ //p是唯一的,将p当作锁代表此时谁拥有商品另一个人就等着
if (i % 2 == 0) {
p.setBrand("哈尔滨");
p.setName("啤酒");
} else {
p.setBrand("费列罗");
p.setName("巧克力");
}
System.out.println("生产者生产了"+p.getBrand()+"----"+p.getName());
}
}
}
}
消费者线程
public class ConsumerThread extends Thread {
private Product p; //消费者消费商品
public ConsumerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (p) {
System.out.println("消费者购买了" + p.getBrand() + "----" + p.getName());
}
}
}
}
测试类
public class Test {
public static void main(String[] args) {
Product p = new Product();
ProducerThread pt = new ProducerThread(p);
ConsumerThread ct = new ConsumerThread(p);
pt.start();
ct.start();
}
}
同步方法解决
使用同步方法会产生一个问题,如果在两个线程内分别加同步方法,实际上这两个对象会产生两把锁,为了保证锁的唯一性,我们把消费和生产可能产生问题的代码都放到商品类中维护,商品对象是唯一的,所以只会有一把锁,保证了线程安全
商品类
public class Product {
private String brand;//品牌
private String name;//名称
//生产商品方法
public synchronized void setProduct(String brand,String name){
this.brand = brand;
this.name = name;
System.out.println("生产者生产了"+this.brand+"----"+this.name);
}
//消费商品方法
public synchronized void getProduct(){
System.out.println("消费者消费了"+this.brand+"----"+this.name);
}
}
消费者
public class ConsumerThread extends Thread {
private Product p; //消费者消费商品
public ConsumerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
p.getProduct();
}
}
}
生产者
public class ProducerThread extends Thread{
private Product p; //生产者线程生产商品
public ProducerThread(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
if(i%2==0)
p.setProduct("费列罗","巧克力");
else
p.setProduct("哈尔滨","啤酒");
}
}
}
实现线程通信(基于synchronizd锁 wait…notify)
在线程中有锁池和等待池,当程序执行到wait()方法时进入到等待池,并解开锁,解锁后另一个线程抢占共享资源,程序执行到notify()时,该方法唤醒等待池中的线程,此时原本在等待池中的程序继续从wait方法向下执行,直到执行到notify()
boolean flag = false; //生产状态 true 代表商品生产成功 false 代表没有商品
//生产商品
public synchronized void setProduct(String brand,String name){
if(flag){
try {
wait(); //有商品进入等待状态并解锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.brand = brand;
this.name = name;
System.out.println("生产者生产了"+this.brand+"----"+this.name);
flag = true; //生产完切换状态有商品
notify(); //通知另一个线程对象
}
//消费商品
public synchronized void getProduct(){
if(!flag){
try {
wait(); //没有商品进入等待状态并解锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了"+this.brand+"----"+this.name);
flag = false;//消费完商品状态切换为空
notify();
}
}
线程池
线程有生命周期,执行完就会死亡,重新创建侵占资源,所以有了线程池,可以维护一定数量的线程对象。可以把线程要执行的代码提交到线程池,由线程池找到可用线程执行代码。比反复创建更节省资源
/*
* newCachedThreadPool 带缓冲线程池
* newFixedThreadPool 指定初始大小线程池
* newScheduledThreadPool 可以延时调度
* newSingleThreadExecutor 单线程线程池
* */
*
// ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService2 = Executors.newFixedThreadPool(10);
// ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
// ExecutorService executorService1 = Executors.newSingleThreadExecutor();
// executorService2.submit(()->{
// MyThread2.myTest();
// });
// //关闭线程池对象
// executorService2.shutdown();
/*
Executors 获得的线程池 参数不同
* ThreadPoolExecutor() 线程池对象
*
* */
// public ThreadPoolExecutor(int corePoolSize, 初始线程池大小
// int maximumPoolSize, 最大允许线程池大小
// long keepAliveTime, 存活时间
// TimeUnit unit, 时间单位
// BlockingQueue workQueue, BlockingQueue
/*
* ArrayBlockingQueue 有界队列
* LinkedBlockingQueue 无界队列
* PriorityBlockingQueue 优先队列
* SynchronousQueue 同步队列
* */
// ThreadFactory threadFactory, thread对象创建工厂
// RejectedExecutionHandler handler) //拒绝策略
public class Demo {
public static void main(String[] args) {
ThreadPoolExecutor myThreadPool = new ThreadPoolExecutor(10, 30,
1000, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//提交线程需要执行的任务到线程池中,并返回该任务的句柄
Future<?> submit = myThreadPool.submit(Test::a);
System.out.println(submit);
myThreadPool.shutdown();
}
}
线程与单例
多线程有可能打破单例,因为可能两个线程同时执行获得对象方法,同时判定对象为空,从而new 出新的对象
解决方法:对象用 volatile 修饰。不会触发重排,双重检测加同步代码块
private volatile static SingleTon sig;
private SingleTon(){};
public static SingleTon getInstance()
if(sig==null){
synchronized (SingleTon.class){
if(sig==null){
sig = new SingleTon();
}
}
}
return sig;
}
}
多个生产者和消费者之间的通信(基于lock锁 await…signal)
核心思想:利用lock锁特点,建立多个阻塞队列,分别放置生产者和消费者,这样无论唤醒队列中的哪一个线程都不会造成多生产或多消费的情况
获得锁
获得队列
打开锁
满足条件开启阻塞队列
等待被唤醒
执行完毕
关闭锁
Lock l = new ReentrantLock();
Condition proCondition = l.newCondition();//生产者队列
Condition csuCondition = l.newCondition();//消费者队列
boolean flag = false; //生产状态 true 代表商品生产成功 false 代表没有商品
//生产商品
public void setProduct(String brand,String name){
l.lock();
try {
if(flag){
//如果有商品,则把生产者加入到生产阻塞队列
try {
proCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.brand = brand;
this.name = name;
System.out.println("生产者生产了"+this.brand+"----"+this.name);
flag = true; //生产完切换状态有商品
// notify(); //通知另一个线程对象
//生产完以后,唤醒消费者队列中任意一个对象
csuCondition.signal();
}finally {
l.unlock();
}
}
//消费商品
public void getProduct(){
l.lock();
try {
if(!flag){
try {
csuCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了"+this.brand+"----"+this.name);
flag = false;//消费完商品状态切换为空
// notify();
proCondition.signal(); //向生产者队列发送信号
}finally {
l.unlock();
}