1.线程基础
进程概念:运行中的程序
线程概念:是进程中的一条执行路径,往往一个进程中会有多条执行路径--多线程
JVM从main方法入口开始执行,这就是一个执行路径,我们叫做主线程;
可以在主线程中,创建其他线程,我们叫做子线程
进程与线程的关系:
线程是cpu调度的基本单位,cpu切片切到谁就谁执行
一个进程可以包含多个线程,至少有一个线程
进程具有独立的资源空间,但是一个进程中的多个线程共享资源
线程的特点: 随机互抢资源
线程的组成:
cpu时间片: 由操作系统分配每个线程执行的时间
运行数据:
堆数据:实例化出来的线程对象
栈数据:线程引用指向实例化的对象
线程的逻辑代码
线程的创建方式:1.创建一个类继承Thread, 2.创建一个类实现Runnable任务
==============线程的创建方式1==============
//案例:主线程和子线程,各打印200次(互抢资源方式)
//分析,1.创建线程继承Thread类,重写run方法,该方法就是子线程执行的区域
//2.实例化子线程对象,调用run方法执行
class MyThread extends Thread{
@Override
public void run() {
for(int i=1;i<=200;i++) {
System.out.println("子线程执行-->"+i);
}
}
}
public class Test1 {
public static void main(String[] args) {
//线程的启动:
//将当前线程对象放入线程组供cpu调度,当cpu调度到你,在内部调用run方法;
//没有调度到你,则是就绪状态
MyThread thread = new MyThread();
thread.start();
//thread.start(); //实例化一个线程对象,多次start 不可以
new MyThread().start();
for(int i=1;i<=200;i++) {
System.out.println("主线程执行-->"+i);
}
}
}
=============线程的创建方式2=============
//创建线程方式2:实现一个任务的方式
//案例:主线程和子线程,各打印200次(互抢资源方式)
//Task实现Runnable任务
class Task implements Runnable {
@Override
public void run() {
for(int i=1;i<=200;i++) {
System.out.println("子线程执行-->"+i);
}
}
}
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(new Task());
thread.start();
for(int i=1;i<=200;i++) {
System.out.println("主线程执行-->"+i);
}
}
}
=====线程状态(基本)=====
1.1 休眠方法
语法: Thread.sleep(毫秒)
用法: 可以用在主线程或子线程中
目的: 在线程中可以复现互抢资源的现象
1.2 线程优先级设置setPriority
线程优先级设置:给线程设置优先级,可以大概率的确定谁先执行完; 但不是绝对的
MyThread thread1 = new MyThread("线程1");
thread1.setPriority(Thread.MIN_PRIORITY); //设置小优先级
thread1.start();
MyThread thread2 = new MyThread("线程2");
thread2.setPriority(Thread.MAX_PRIORITY); //设置优先级,再启动
thread2.start();
1.3. 线程的礼让
线程的礼让: yield
设置了礼让的线程,会将线程资源让一次出去,继续跟其他线程争抢;
会影响执行效率,但不是绝对性的
class A extends Thread{
@Override
public void run() {
for(int i=1;i<=200;i++) {
System.out.println("设置礼让线程1===>"+i);
Thread.yield(); //礼让
}
}
}
同样的设置B
1.4 线程的合并
线程的合并(插队)--join
在线程中设置了插队后,插队的线程绝对性地先执行完;
============线程状态(等待)===============
2. 线程安全
2.1 多线程存储数组元素的数据安全问题
线程安全案例:
案例:在线程中给定一个数组,两个线程同时往数组中存元素
分析:先执行的线程应该存储第一个下标位置,后执行的线程,应该存储第二个下标位置
问题:还没来得及下标的累加,可能都存储到了第一个位置---因为线程具有随机互抢特性
//代码应用:
//问题1:打印数组时,可能数据还没有存储
//解决: 想个办法先执行子线程的存储;再执行主线程的打印---join
//问题2:new两个对象,线程中对象的属性都有两份,没办法共享数据
//解决:将线程的属性变为static
//问题3:数据出现问题,都存储到第一个位置了
//加锁:
//锁: 同步代码块,同步方法
//同步代码块: synchronized("lock") {}
//锁的注意事项: 1.两个线程使用同一把锁(同一个锁对象),2.锁的范围
class MyThread extends Thread{
static String[] s = {"","","","",""}; //数据存数据
static int index; //下标
String value;
public MyThread(String value) {
this.value = value;
}
@Override
public void run() {
//只有一个线程能进入锁,另一个在外面等待,等待锁的线程执行完毕,另一个才能进入
synchronized("lock") { //静态属性,字符串常量
s[index] = value; //将存储与下标累加操作进行捆绑
//睡眠3毫秒,复现有问题的数据
index++;
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread my1 = new MyThread("hello");
my1.start();
MyThread my2 = new MyThread("world");
my2.start();
my1.join(); //两个子线程都要插主线程的队
my2.join();
//打印数组:
System.out.println(Arrays.toString(MyThread.s));
}
}
2.2 继承Thread方式实现线程安全
卖票问题:
*问题1: 每个对象都会卖1000张,共5000张
*解决方案: ticket属性+static修饰,变为了5个线程只卖1000张
*
*问题2:出现部分重票的问题---数据安全问题
*解决方案: 加锁
*锁的注意事项:
*1.同一把锁(静态属性,常量值)
*2.锁的范围,锁整个while还是whlie里面的语句(锁住里面,如果锁while,则只有一个窗口卖)
*
*问题3: 会出现负数和0? 临界点问题
*原因: 当票到达1张时,所有线程都可进入到while判断ticket>0,只是一个线程进入锁,其他在锁外等待
*解决: 在锁内部加入if(ticket>0)的判断
*
*问题4: 优化,有多少个窗口卖,必须有多少个窗口退出
*解决方案:加入else提示,将while做一个死循环,在else中break
class MyThread extends Thread{
private static int ticket = 1000;
public MyThread(String name) {
super(name);
}
@Override
public void run() {
//锁方式1:同步代码块
/*
while(true) {
synchronized ("lock") {
if(ticket>0) {
System.out.println(super.getName()+"窗口正在卖第"+ticket+"张票");
ticket--;
}else {
System.out.println(super.getName()+"窗口已经卖完了");
break;
}
}
}*/
//锁方式2: 同步方法:也要注意同一把锁(调用方法的对象-this有问题,需加static),和锁的范围
while(true) {
if(save()) { //在同步方法调用中要进行返回值判断
break;
}
}
}
//同步方法的实现
private static synchronized boolean save() {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"窗口正在卖第"+ticket+"张票");
ticket--;
return false;
}else {
System.out.println(Thread.currentThread().getName()+"窗口已经卖完了");
return true;
}
}
}
2.3 实现任务方式完成线程安全
通过实现任务的方式完成卖票系统的案例:
分析:创建一个Task类实现Runnable接口
与继承Thread的区别
1.多个线程操作同一个Task任务,所以属性无需加static
2.同步代码块的锁对象可以用this
3.同步方法无需加static
class Task implements Runnable{
private int ticket = 1000;
@Override
public void run() {
/*
while(true) {
//锁方式1:同步代码块
synchronized (this) {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"窗口正在卖第"+ticket+"张票");
ticket--;
}else {
System.out.println(Thread.currentThread().getName()+"窗口已经卖完了");
break;
}
}
}
*/
//锁方式2: 同步方法:也要注意同一把锁(调用方法的对象-this有问题,需加static),和锁的范围
while(true) {
if(save()) { //在同步方法调用中要进行返回值判断
break;
}
}
}
//同步方法的实现
private synchronized boolean save() {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"窗口正在卖第"+ticket+"张票");
ticket--;
return false;
}else {
System.out.println(Thread.currentThread().getName()+"窗口已经卖完了");
return true;
}
}
}
3. 死锁
死锁案例: 线程的双方都握着对方的资源,都退不出去,最后形成了死锁---锁嵌套
分析:
创建一个类继承Thread,里面有一个属性,用于做判断的
实例化两个线程,传递参数,1个为true,一个为false
判断中,一个先执行A锁,一个先执行B锁的锁嵌套
注意:学习死锁的目的,就是为了规避死锁
class MyThread extends Thread{
private boolean flag;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag) {
synchronized ("A") {
System.out.println(super.getName()+"--进入了A锁");
synchronized ("B") {
System.out.println(super.getName()+"--进入了B锁");
}
}
}else {
synchronized ("B") {
System.out.println(super.getName()+"--进入了B锁");
synchronized ("A") {
System.out.println(super.getName()+"--进入了A锁");
}
}
}
}
}
4.生产者消费者模型
技术点:
多线程模型,安全锁机制,等待与唤醒
生产者负责生产
消费者负责消费
我们需要准备一个仓库存钱,消费者看库存,库存有钱才能消费
库存没钱---等待消费
库存满了---生产者停止生产,等待消费者消费后,有库存空间才挣
============代码案例===========
/**
多线程 生产与消费模型:
分析:
1,创建生产者类继承Thread,只负责生产
2. 创建消费者类继承Thread,只负责消费
3. 两个线程共同操作仓库类Store
4. 加入安全锁及等待与唤醒机制
问题1:生产两个动作,消费也是两个动作,当你打印生产,还没加库存;就已经消费了,库存值不对
处理:加锁 ,解决生产与消费的数据混乱
问题2: 仓满和仓空的限制
处理: 限制仓满,生产者等待(wait);仓空,消费者等待(wait)
细节:
1. 等待时,会将资源交出去
2. 锁对象与等待唤醒的对象是同一个
扩展: 多个生产与多个消费情况
判断中改为while
*
*/
public class Test1 {
public static void main(String[] args) {
Store store = new Store();
new Producter(store).start();
new Customer(store).start();
//多个生产者和消费者线程
new Producter(store).start();
new Customer(store).start();
}
}
-----生产者-----
public class Producter extends Thread {
private Store store;
public Producter(Store store){ //库存要存进来
this.store = store;
}
@Override
public void run() {
while(true) {
try {
store.push();
} catch (InterruptedException e) {
e.printStackTrace();
} //生产者负责生产
}
}
}
----消费者----
public class Customer extends Thread {
private Store store;
public Customer(Store store) {
this.store = store;
}
@Override
public void run() {
while(true) {
try {
store.pop();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //消费者负责消费
}
}
}
---库存---
public class Store {
private int num; //单位万
public void push() throws InterruptedException { //库存生产
synchronized (this) {
//生产20件货,就满了,停止生产
while(num>=20) {
this.wait();
}
num++;
System.out.println("生产者已经生产了第"+num+"件货");
this.notify(); //唤醒等待的线程
}
}
public void pop() throws InterruptedException { //库存消费
synchronized (this) { //
while(num<=0) {
this.wait(); //等待消费,等待时,相当于把自身资源交出去了
}
System.out.println("消费者已经消费了第"+num+"件货");
num--;
this.notify(); //唤醒等待的线程
}
}
}
===========线程状态(阻塞)===========
5.多线程高级
5.1 线程池(重点)
概念:可以认为是线程对象的容器,预先创建多个线程对象,然后根据这些对象可以实现复用
好处:减少创建和销毁线程的数目,提升性能
复用机制: 用完了线程对象后,重新回收到线程池中
class Task implements Runnable{
@Override
public void run() {
for(int i=1;i<=10;i++) {
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
};
}
public static void main(String[] args) {
//创建一个单个线程的线程池对象
//ExecutorService es = Executors.newSingleThreadExecutor();
//创建一个带缓冲区的线程池对象,灵活控制线程对象,有多少任务,我就创建多少个线程对象
//ExecutorService es = Executors.newCachedThreadPool();
//创建固定个数的线程池对象,如果任务过多,则用回收线程对象去处理多余任务
ExecutorService es = Executors.newFixedThreadPool(2);
Task task = new Task();
es.submit(task); //类似,线程.start()
es.submit(task);
es.submit(task); //谁先用完并回收,就谁去执行新任务
es.shutdown(); //关闭线程池
}
5.2 Callable接口
Callable接口: 和Runnable类似,也是处理任务的接口
区别: Callable是带返回值的接口,可以将线程中处理完的数据返回
Future接口,该对象接收submit的返回值,可以求出call返回值
//案例: 使用两个线程计算1~50,51~100的和,并进行汇总
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> future = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i=1;i<=50;i++) {
sum += i;
}
return sum;
}
});
Future<Integer> future2 = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i=51;i<=100;i++) {
sum += i;
}
return sum;
}
});
//get方法类似线程基础的join,在主线程中阻塞
int sum = future.get()+future2.get();
System.out.println("两个线程的和:"+sum);
5.3 重入锁
重入锁:Lock接口 实现类:ReentrantLock
与synchronized类似,用于加锁的
提供了两个方法: lock()-获取锁 unlock()-释放锁
向字符串数组中写入值,避免在3,4 的位置重复写入
class MyList{
String[] ss = {"A","B","",""};
int index = 2;
Lock lock = new ReentrantLock();
public void add(String value) {
//有多个线程可以调用add方法
lock.lock(); //加锁
try {
ss[index] = value;
index++;
} finally {
lock.unlock();
}
}
}
5.4 读写锁
读写锁:一般用在读远远高于写的情况,可以提升线程执行的性能
读写锁,在读与读之间是不互斥,不阻塞的,所以性能会提升
class MyClass{
//加入读写锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReadLock rl = readWriteLock.readLock();
WriteLock wl = readWriteLock.writeLock();
Integer value;
public Integer getValue() {
try {
rl.lock();
return value;
} finally {
rl.unlock();
}
}
public void setValue(Integer value) {
try {
wl.lock();
this.value = value;
} finally {
wl.unlock();
}
}
}
public class Test2 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
Runnable rt = new Runnable() {
@Override
public void run() {
myClass.getValue();
}
};
Runnable wt = new Runnable() {
@Override
public void run() {
myClass.setValue(1);
}
};
ExecutorService es = Executors.newFixedThreadPool(20);
long start = System.currentTimeMillis();
for(int i=0;i<18;i++) {
es.submit(rt); //18个线程用于读操作
}
for(int i=0;i<2;i++) {
es.submit(wt); //2个线程用于写操作
}
es.shutdown();
while(!es.isTerminated()){} //阻塞,确保所有线程结束后,才往下走
System.out.println(System.currentTimeMillis()-start);
}
}
6.线程安全的集合
6.1 Collections提供的安全集合
Collections工具类中提供了针对List,Set,Map的安全集合
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
当然他们的安全机制性能方面有比较大的影响,一般我们会用后续并发安全集合
6.2 CopyOnWriteArrayList
CopyOnWriteArrayList: ArrayList的安全集合(读写并发)
内部进行了加锁,且性能方面比Collestions提供的安全集合更高
该安全集合具有读写分离的特性
读无锁,写有锁,读写之间不互斥,不阻塞,在内部拷贝了一份副本存数据
往往也是用在读多写上的情况
6.3 CopyOnWriteArraySet
CopyOnWriteArraySet: 安全的set集合,底层实现通过CopyOnWriteArrayList
该类也是读写分离的set集合类,该类具备存储唯一性
6.4.线程安全的HashMap
ConcurrentHashMap: 并发的HashMap(前提是安全)
Coolections中也提供了线程安全的Map,只不过锁的是整个hash表
7.队列
7.1 Queue队列接口
Queue:队列的接口,特点:先进先出
LinkedList实现类就实现了该接口
Queue<Integer> queue = new LinkedList<Integer>();
//如果队列没有元素,则返回null
queue.offer(1);
queue.offer(3);
queue.offer(2);
System.out.println(queue.peek()); //取元素不移除
System.out.println(queue.poll()); //取元素并移除
System.out.println(queue);
7.2ConcurrentLinkedQueue实现类
ConcurrentLinkedQueue: 在多线程中,性能最高的并发的队列
内部采用了CAS无锁交换算法进行存储
有3个参数: V(要改变的变量) E(预期值) N(新值)
执行过程中,如果V=E,那么N就可以改变V变量的值了
Queue<Integer> queue = new ConcurrentLinkedQueue<Integer>();
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=5;i++) {
queue.offer(i);
}
}
});
th1.start();
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=6;i<=10;i++) {
queue.offer(i);
}
}
});
th2.start();
th1.join(); th2.join();
for(int i=1;i<=10;i++) {
System.out.println(queue.poll());
}
7.3 阻塞队列
BlockingQueue:阻塞队列的接口
实现类有: 有界队列ArrayBlockingQueue(推荐),无界队列LinkedBlockingQueue
有界队列是有长度限制 无界队列可以认为没有
有两个阻塞方法: put,take ,该阻塞方法的使用类似于生产者消费者用法
put:一般不能超过有界队列长度,超过了阻塞 take,没有值时阻塞