Java--多线程学习

什么是进程

正在运行的程序,是系统进行资源分配的基本单位。
目前操作系统支持多进程,可以同时执行多个进程,通过进程PID进行区分
单核CPU在同一时刻,只能运行一个进程;宏观并性、微观串行

什么是线程

线程,又称轻量级进程(Light Weight Process)。程序中的一个顺序控制流程,同时也是CPU的基本调度单位。进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程。

进程和线程的区别

·进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。· 一个程序运行后至少有一个进程。
·一个进程可以包含多个线程,但是至少需要有一个线程。·进程间不能共享数据段地址,但同进程的线程之间可以。

线程的组成

·任何一个线程都具有基本的组成部分∶

  • CPU时间片∶操作系统(OS)会为每个线程分配执行时间。
  • 运行数据∶
    • 堆空间∶存储线程需使用的对象,多个线程可以共享堆中的对象。·
    • 栈空间∶存储线程需使用的局部变量,每个线程都拥有独立的栈。
·
  • 线程的逻辑代码。

线程的特点

  1. 线程抢占式执行

    • 效率高
    • 可防止单一前程长时间独占CPU
  2. 在单核CPU中,宏观上同时执行,微观上顺序执行。

创建线程

继承Thread

  1. 继承Thread类,重写run方法
    1. 继承Thread类,
    2. 覆盖run方法
    3. 创建子类对象
    4. 调用start方法

获取线程的名称

package com.city.test;

public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        MyThread myThread1=new MyThread();
        //启动线程不能调用run方法
        myThread.start();

        for (int i =0;i<50;i++)
        System.out.println("主线程"+i);

    }
}

package com.city.test;

public class MyThread extends  Thread{

    @Override
     public void run() {
        for (int i =0;i<50;i++)
        System.out.println("ID:"+this.getId()+" name:"+this.getName()+"运行子线程"+i++);
    }
}

每次结果都不一样
主线程0
主线程1
主线程2
ID:12 name:Thread-0运行子线程0
主线程3
主线程4
主线程5
ID:12 name:Thread-0运行子线程2
主线程6
ID:12 name:Thread-0运行子线程4
主线程7
主线程8
主线程9
ID:12 name:Thread-0运行子线程6
主线程10
主线程11
主线程12
主线程13
主线程14
主线程15
主线程16
ID:12 name:Thread-0运行子线程8
主线程17
主线程18
主线程19
主线程20
主线程21
ID:12 name:Thread-0运行子线程10
主线程22
主线程23
主线程24
主线程25
主线程26
ID:12 name:Thread-0运行子线程12
主线程27
主线程28
主线程29
主线程30
主线程31
主线程32
主线程33
主线程34
主线程35
主线程36
主线程37
主线程38
ID:12 name:Thread-0运行子线程14
主线程39
ID:12 name:Thread-0运行子线程16
主线程40
ID:12 name:Thread-0运行子线程18
主线程41
主线程42
主线程43
主线程44
主线程45
主线程46
主线程47
主线程48
ID:12 name:Thread-0运行子线程20
主线程49
ID:12 name:Thread-0运行子线程22
ID:12 name:Thread-0运行子线程24
ID:12 name:Thread-0运行子线程26
ID:12 name:Thread-0运行子线程28
ID:12 name:Thread-0运行子线程30
ID:12 name:Thread-0运行子线程32
ID:12 name:Thread-0运行子线程34
ID:12 name:Thread-0运行子线程36
ID:12 name:Thread-0运行子线程38
ID:12 name:Thread-0运行子线程40
ID:12 name:Thread-0运行子线程42
ID:12 name:Thread-0运行子线程44
ID:12 name:Thread-0运行子线程46
ID:12 name:Thread-0运行子线程48

或者可以用Thread.currentThread().getId() 获取当前线程ID
和Thread.currentThread().getName()获取线程名称
若修改,只能修改线程的名称

修改线程的名称

  1. 调用线程对象的setName方法(要在线程启动之前)
  2. 或者使用线程子类的构造方法赋值
public MyThread(String name){
	super(name);
	}

买票模拟

public class Demo1 {
    public static void main(String[] args) {
        TicketWin ticketWin =new TicketWin("1号窗口");
        TicketWin ticketWin1 =new TicketWin("2号窗口");
        TicketWin ticketWin2 =new TicketWin("3号窗口");
        TicketWin ticketWin3 =new TicketWin("4号窗口");
        ticketWin.start();
        ticketWin1.start();
        ticketWin2.start();
        ticketWin3.start();
    }
}

package com.city.test;

public class TicketWin extends  Thread{
    private int ticket=10;
    public TicketWin(){}
    public TicketWin(String name){
        super(name);
    }

    @Override
    public void run() {
       while(true){
           if(ticket<=0){
               System.out.println(Thread.currentThread().getName()+"票卖完了");
               break;
           }
           System.out.println(Thread.currentThread().getName()+"卖了第"+ticket--+"张票");
       }
    }
}

1号窗口卖了第10张票
4号窗口卖了第10张票
3号窗口卖了第10张票
2号窗口卖了第10张票
3号窗口卖了第9张票
4号窗口卖了第9张票
4号窗口卖了第8张票
4号窗口卖了第7张票
4号窗口卖了第6张票
1号窗口卖了第9张票
4号窗口卖了第5张票
3号窗口卖了第8张票
2号窗口卖了第9张票
3号窗口卖了第7张票
4号窗口卖了第4张票
1号窗口卖了第8张票
4号窗口卖了第3张票
4号窗口卖了第2张票
3号窗口卖了第6张票
2号窗口卖了第8张票
3号窗口卖了第5张票
3号窗口卖了第4张票
3号窗口卖了第3张票
3号窗口卖了第2张票
3号窗口卖了第1张票
4号窗口卖了第1张票
1号窗口卖了第7张票
1号窗口卖了第6张票
1号窗口卖了第5张票
1号窗口卖了第4张票
4号窗口票卖完了
3号窗口票卖完了
2号窗口卖了第7张票
2号窗口卖了第6张票
2号窗口卖了第5张票
2号窗口卖了第4张票
2号窗口卖了第3张票
2号窗口卖了第2张票
2号窗口卖了第1张票
2号窗口票卖完了
1号窗口卖了第3张票
1号窗口卖了第2张票
1号窗口卖了第1张票
1号窗口票卖完了

实现Runnable接口

  1. 实现Runnable接口
  2. 实现run方法
  3. 创建实现类对象
  4. 将实现类对象传入创建的线程对象
  5. 通过线程对象调用start
public class Demo1 {
    public static void main(String[] args) {
        TicketWin ticketWin =new TicketWin();
        TicketWin ticketWin2 =new TicketWin();
        Thread thread=new Thread(ticketWin,"1号窗口");
        Thread thread2=new Thread(ticketWin2,"二号窗口");

        //匿名内部类方式
        Runnable runnable=new Runnable() {
            private int ticket=10;
            @Override
            public void run() {
                while(true){
                    if(ticket<=0){
                        System.out.println(Thread.currentThread().getName()+"票卖完了");
                        break;
                    }
                    System.out.println(Thread.currentThread().getName()+"卖了第"+ticket--+"张票");
                }
            }
        };
        Thread thread3=new Thread(runnable,"3号窗口");
        thread.start();
        thread2.start();
        thread3.start();
    }
}
package com.city.test;

public class TicketWin implements Runnable{
   private int ticket=10;

    @Override
    public void run() {
       while(true){
           if(ticket<=0){
               System.out.println(Thread.currentThread().getName()+"票卖完了");
               break;
           }
           System.out.println(Thread.currentThread().getName()+"卖了第"+ticket--+"张票");
       }
    }
}

线程的状态

  1. new 初始状态:
    线程对象被创建,即为初始状态,只在堆中开辟内存,与常规对象无异
  2. Ready就绪状态:
    调用start之后,进入了就绪状态。等待OS选中后,并分配时间片。
  3. Running 运行状态:
    获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态
  4. Terminated终止状态
    主线程main或独立线程run结束,进入终止状态,并释放持有的时间片。

常见方法

  1. 休眠
    • public static void sleep(long millis);
    • 当前线程主动休眠millis毫秒
  2. 放弃
    • public static void yield()
    • 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
  3. 加入
    • public final void join()
    • 允许其他线程加入到当前线程中,并阻塞当前线程,直到加入线程执行完毕。
  4. 设置线程优先级
    • 优先级:
      • 线程对象.setPriority()
      • 线程优先级为1-10 ,默认为5,优先级越高,表示获取CPU机会越多
    • 守护线程
      • 线程对象.setDaemon(true);设置为守护线程
      • 线程有两类,用户级线程(前台线程)、守护线程(后台线程)
      • 如果程序中所有前台进程都执行完毕了,后台线程就会自动结束
      • 垃圾回收器线程属于守护线程

多线程的安全问题

有两个线程A,B,一个长度为5的数组,第一个线程往数组存储hello,第二个线程往数组存储world,第一个问题就是,A线程刚刚查找到下标,时间片到期,B线程 强到CPU,查找下标,和A线程是一个下标,存到了下标0的位置,A线程强到CPU,他也存在了下标0中,最后数据只看到了hello。
即当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省

同步代码块

保证线程的安全性。

synchronized (临界资源对象){
	//代码
}

注:每个对象都有一个互斥锁标记,用来分配给线程的。
只有拥有对象互斥锁标记的线程。才能进入堆该对象加锁的同步代码块。
线程退出同步代码块时,会释放相应的互斥锁标记。
JDK1.5之后就绪与运行统称为Runnable(运行状态)

同步方法

synchronized 返回值类型 方法名称(形参列表){
当前对象 this加锁
代码
}
如果是静态方法,锁就是当前这个类。Ticket.class

同步规则

  • 只有在调用同步代码块的方法,或者同步方法时,才需要对象的锁标记
  • 如果调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
    已知JDK中线程安全类
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchonized修饰的同步方法

死锁:

当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
例如哲学家进餐问题

线程通信

  • 等待
    • public final void wait()
    • public final void wait(long timeout)
    • 必须对obj加锁的同步代码块中,在一个线程中,调用obj。wait()时,此线程会释放其拥有的所有锁标记,同时此线程阻塞在o的等待队列中,释放锁,进入等待队列
  • 通知
    • public final void notify()
    • public final void notifyAll();

生产者消费者

·若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

高级多线程

线程池的概念

问题:

  • 线程是宝贵的内存资源,单个线程约占1MB空间,过多分配易造成内存异常。
  • 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销、照成程序性能下降。
    线程池:
  • 线程容器,可设定线程分配的数量上限
  • 将预先创建的线程对象存储池中,并重用线程池中的线程对象
  • 避免频繁的创建和销毁

线程池原理

将任务提交给线程池,有现成池分配线程、运行任务,并在当前任务结束后复用线程

创建线程池

常用的线程池接口和类(所在包java.util.concurrent)∶

  • Executor:线程池的顶级接口
  • ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码。
  • Executors工厂类:通过此类可以获得一个线程池。
  • 通过 newFixedThreadPoo(int nThreads)获取固定数量的线程池。参数∶指定线
程池中线程的数量
  • 通过newCachedThreadPool()获得动态数量的线程池,如不够则创建新的,无上限。
package package2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        //固定线程池,个数为4
      // ExecutorService executorService= Executors.newFixedThreadPool(4);
        //线程个数由任务来决定。
       ExecutorService executorService=Executors.newCachedThreadPool();
       //单线程池
       // Executors.newSingleThreadExecutor();
        //调度线程,周期定时的执行
     //   Executors.newScheduledThreadPool(corePoolSize);
        //提交任务
        Runnable runnable=new Runnable() {
            private int ticket=100;
            @Override
            public void run() {
                while(true){
                    if (ticket<=0){
                        break;
                    }
                    System.out.println(Thread.currentThread().getName()+"买了第"+ticket-- +"张票");
                }
            }
        };

        for(int i=0;i<4;i++)
        executorService.submit(runnable);

        //关闭线程池,一定要关闭,否则会一直等待submit
        executorService.shutdown();//等待任务执行完毕,启动一次顺序关闭,执行以前提交的任务,但不结束新任务
       // executorService.shutdownNow();//立即结束,直接关闭,试图停止所有在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
    }

}

Callable接口

可调用的对象。

public interface Callable<V>{
	public V call() throws Exception;
}

JDK5之后加入,与Runnable接口类似,实现之后代表一个线程任务。
Callable具有泛型返回值、可以声明异常。

Future:

表示将要完成任务的结果
用get方法获取结果

  • 表示ExecutorService.submit()所返回的状态结果,就是call()返回值
  • 方法: V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)

线程的同步和异步

同步

形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续
在这里插入图片描述
注:单挑执行路径

异步

形容一次方法调用,异步一旦开始,像是一次信息传递,调用者告知后立即返回,二者竞争时间片,并发执行
在这里插入图片描述
多条执行路径

Lock接口

用于实现同步的API

  • 与synchronized比较,显示定义,结构更灵活
  • 提供更多实用性方法,功能更强大、性能更优越
  • 常用方法
    • void Lock() 获取锁,如锁被占用,则等待
    • boolean tryLock() 尝试获取锁(成功返回true,失败返回false,不阻塞)
    • void unlock() 释放锁

重入锁(Lock实现类)

ReentrantLock :一个可重入的互斥锁Lock,与synchronized一样具有互斥锁功能

package package2;

import org.omg.CORBA.PRIVATE_MEMBER;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyList {
    private Lock lock= new ReentrantLock();
    private String[] strings={"A","B","","",""};
    private int count=2;
    public void add(String value){
        lock.lock();
        try {

            strings[count]=value;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }finally {
            lock.unlock();
        }
    }
    public String[] getStrings(){
        return this.strings;
    }

}

读写锁

ReentrantReadWriteLock:
维护一对相关的锁,一个用于只读,一个用于写入。写入锁是独占的

  • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
  • 支持多次分配读锁,使多个都操作可以并发执行。
    写-写∶互斥,阻塞。

    读-写∶互斥,读阻塞写、写阻塞读。
    读-读∶不互斥、不阻塞。

    在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。

    isTerminated()为true表示线程池执行完毕,反之亦然。
    while(es.isTerminated()){
    }//打空转

线程安全的集合

在这里插入图片描述
在这里插入图片描述

package demo1;

import java.lang.reflect.Array;
import java.util.ArrayList;

public class demo1 {
    public static void main(String[] args) {
        ArrayList<String> arrayList=new ArrayList();
        for (int i=0;i<20;i++){
            int temp=i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0;j<30;j++){
                        arrayList.add(Thread.currentThread().getName()+"=="+temp+"=="+j);
                        System.out.println(arrayList.toString());
                    }
                }
            }).start();
        }
    }
}

结果出现线程并发修改异常,jdk提供了以下的方法可以获得线程安全集合,所在位置Collections工具栏
在这里插入图片描述
jdk1.2提供,接口统一、维护性高,但性能没有提升,均以synchonized实现

CopyOnWriteArrayList

线程安全的ArrayList,加强版的读写分离
写有锁,都无锁,读写之间不阻塞,优于读写锁
写入时,先copy一个容器副本,在添加新元素,最后替换引用
使用方式和ArrayList无异
源码

 final transient ReentrantLock lock = new ReentrantLock();//Lock锁
   private transient volatile Object[] array;
    final void setArray(Object[] a) {
        array = a;
    }
     public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

CopyOnWriteArraySet

线程安全set,底层使用CopyOnWriteArrayList实现
唯一不同在于,使用addIfAbsent()添加元素,会遍历数组
如存在元素,则不添加(扔掉副本)

Queue接口(队列)

常用方法:
在这里插入图片描述

ConcurrentLinkedQueue

线程安全、可高效读写的队列,高并发下性能最好的队列。
无锁、CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)
V:要更新的遍历、E:预期值、N:新值。
只有当V==E时,V=N,否则表示已经被更新过,取消当前操作

BlockingQueue接口,阻塞队列

queue的子接口,阻塞队列,增加了两个线程状态为无限期等待的方法
在这里插入图片描述
在这里插入图片描述

ConcurrentHashMap

初始容量为16段,使用分段所设计
不对整个map加锁,而是为每个segment加锁。
最理想状态为16个对象分别存储16个 Segment,并行数量16
使用方式与HashMap无异

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值