1.set接口
- set接口父接口为Collection接口
- set集合无序,不能存入重复元素
- set集合没有下标
/**本类用于测试Set*/
public class TestSet {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("紫霞仙子");
set.add("至尊宝");
set.add("蜘蛛精");
set.add("紫霞仙子");
set.add(null);
set.add(null);
/*1.set集合中的元素都是没有顺序的
* 2.set集合中的元素不能重复
* 3.set集合可以存null值,但是最多只有一个*/
System.out.println(set);
System.out.println(set.contains("唐僧")); //false,判断是否包含指定元素
System.out.println(set.isEmpty());//false,判断是否为空
System.out.println(set.remove(null));//true,删除指定元素
System.out.println(set);
System.out.println(set.size());//3,获取集合中元素的个数
System.out.println(Arrays.toString(set.toArray()));//将集合转换为数组
Set<String> set2 = new HashSet<>();
set2.add("小兔子");
set2.add("小老虎");
set2.add("小海豚");
set2.add("大角牛");
System.out.println(set2);
System.out.println(set.addAll(set2));//将set2集合的所有元素添加到set集合中
System.out.println(set.contains(set2));//判断set集合中是否包含set2集合的所有元素
System.out.println(set.removeAll(set2));//删除set集合中属于set2集合的元素
System.out.println(set.retainAll(set2));//只保留set和set2集合的公共元素
System.out.println(set);
}
}
/**本类用于进一步测试Set*/
public class TestSet2 {
public static void main(String[] args) {
Set<Student> set = new HashSet<>();
Student s1 = new Student("张三",3);
Student s2 = new Student("李四",4);
Student s3 = new Student("李四",4);
set.add(s1);
set.add(s2);
set.add(s3);
/*如果set中存放的是自定义的类型
* 需要给自定义类中添加重写的equals()与hashCode(),才会去重
* 不然会认为s2和s3的地址值不同,是两个不同的对象,不会去重*/
System.out.println(set);
}
}
class Student1 {
String name;
int id;
public Student1(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "Student1{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student1 student1 = (Student1) o;
return id == student1.id &&
Objects.equals(name, student1.name);
}
@Override
public int hashCode() {
return Objects.hash(name, id);
}
}
2.进程与线程
2.1 进程的特点
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
- 并发性:多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.
2.1.1 进程、程序之间的区别
- 程序:数据与指令的集合,程序是静态的
- 进程:给程序加入了时间的概念,不同的时间进程有不同的状态
- 进程是动态的,就代表OS中正在运行的程序
2.1.2 并行、串行、并发
CPU:电脑的核心处理器,类似于“大脑”
- 并行:相对来说资源比较充足,多个CPU可以同时处理不同的多件事,类似于多车道
- 串行:是指同一时刻一个CPU只能处理一件事,类似于单车道
- 并发:相对来说资源比较紧缺,多个进程同时抢占公共资源,比如多个进程抢占一个CPU
2.2 线程的特点
- 线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
- 一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
- 多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。
- 在宏观上,一个CPU看似可以同时处理多件事
在微观上,一个CPU同一时刻只能处理一件事
结论:线程的执行具有随机性,我们控制不了,是由OS底层的算法来决定的
2.2.1 线程与进程之间的关系
一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
2.3 线程的五种状态
- 就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可立即执行
- 执行(运行)状态:线程已经获得CPU,其程序正在运行的状态
- 阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞
- 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
- 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
2.4 多线程的创建方式
- 方式一:继承Thread类
/**本类用于多线程编程实现方案一:继承Thread类来完成*/
public class TestThread1 {
public static void main(String[] args) {
/*4.new对应的是线程的新建状态*/
/*5.要想模拟多线程,至少得启动2个线程,如果只启动1个,是单线程程序*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
/*6.这个run()如果直接这样调用,是没有多线程抢占执行效果
* 只是把这两句话看作普通方法得调用,谁先写,就先执行谁*/
// t1.run();
// t2.run();
/*7.start()对应得状态就是就绪状态,会把刚刚新建好的线程加入就绪队列之中
* 至于什么时候执行,就是多线程执行的效果,需要等待OS选中分配CPU
* 8.执行的时候start()底层会自动调用我们重写的run()中的业务
* 9.线程的执行具有随机性,也就是说t1-t4具体怎么执行
* 取决于CPU的调度时间片的分配,我们是决定不了的*/
t1.start();//以多线程的方式启动线程1,将当前线程变为就绪状态
t2.start();
t3.start();
t4.start();
}
}
class MyThread extends Thread{
/*1.多线程实现的方案1:通过继承Thread类,并重写run()完成的*/
@Override
public void run() {
/*2.super.run()表示的是调用父类的业务,我们现在要用自己的业务,所以注释掉*/
//super.run();
for (int i = 0; i < 10 ; i++) {
/*3.getName()表示可以获取当前正在执行的线程的名称
* 由于本类继承了Thread类,所以可以直接使用这个方法*/
System.out.println(i+"="+getName());
}
}
}
方式二:实现Runnable接口
package cn.tedu.thread;
/*本类用于多线程编程实现方案二:实现Runnable接口来完成*/
public class TestThread2 {
public static void main(String[] args) {
//5.创建自定义类的对象--目标业务类对象
MyRunnable target = new MyRunnable();
//6.如何启动线程?自己没有,需要与Thread建立关系
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target,"小灰灰");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定义多线程类
class MyRunnable implements Runnable{
//2.添加父接口中的抽象方法run(),里面是自己的业务
@Override
public void run() {
//3.写业务,打印10次当前正在执行的线程名称
for (int i = 0; i < 10; i++) {
/*问题:自定义类与父接口Runnable中都没有获取名字的方法
* 所以还需要从Thread中找:
* currentThread():静态方法,获取当前正在执行的线程对象
* getName():获取当前线程的名称*/
System.out.println(i+"="+Thread.currentThread().getName());
}
}
}
2.5 售票案例
方式一:
/*需求:设计多线程编程模型,4个窗口共计售票100张
* 本方案使用多线程编程方案1,继承Thread类的方式来完成*/
public class TestThreadV2 {
public static void main(String[] args) {
//5.创建多个线程对象
TicketThread2 t1 = new TicketThread2();
TicketThread2 t2 = new TicketThread2();
TicketThread2 t3 = new TicketThread2();
TicketThread2 t4 = new TicketThread2();
//6.以多线程的方式启动
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*1.多线程中出现数据安全问题的原因:多线程程序+有共享数据+多条语句操作共享数据*/
/*2.同步锁:相当于给容易出现问题的代码加了一把锁,加锁以后,这些代码实现同步效果
* 同步就是排队,需要考虑两个问题:
* 1)加锁的范围:不能太大,太大,效率太低;也不能太小,太小,锁不住
* 2)多个线程对象必须是同一把锁,锁对象只有一个,不然锁不住*/
//1.自定义多线程售票类,继承Thread
class TicketThread2 extends Thread{
//3.定义变量,保存要售卖的票数
/*问题:4个线程对象共计售票400张,原因是创建了4次对象,各自操作各自的成员变量
* 解决:让所有对象共享同一个数据,票数需要设置为静态*/
static int tickets = 100;
//创建锁对象,锁对象的类型不做限制,但是得唯一
//Object o = new Object();
//2.重写父类的run(),里面是我们的业务
@Override
public void run() {
//4.1循环卖票
while(true){
//当前类的字节码对象,这个是肯定唯一的
synchronized (TicketThread2.class) {//解决的是重卖
if(tickets > 0){//双重校验,解决超卖
try {
//7.让每个线程经历休眠,增加线程状态切换的频率与出错的概率
//问题1:产生了重卖的现象:同一张票卖了多个人
//问题2:产生了超卖的现象:超出了规定的票数100,出现了0 -1 -2这样的票
Thread.sleep(10);//让当前线程休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.2打印当前正在卖票的线程名称,并且票数-1
System.out.println(getName() + "=" + tickets--);
}
//4.3做判断,如果没有票了,就退出死循环
if (tickets <= 0) break;//注意,死循环一定要设置出口
}
}
}
}
方式二:
/*需求:设计多线程编程模型,4个窗口共计售票100张
* 本方案使用多线程编程方案2,实现Runnable接口的方式来完成*/
public class TestRunnableV2 {
public static void main(String[] args) {
//5.创建Runnable接口的实现类对象,作为目标业务对象
TicketRunnable2 target = new TicketRunnable2();
//6.创建多个Thread类线程对象,并将target业务对象交给多个线程对象来处理
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
//7.以多线程的方式启动多个线程对象
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定义多线程类实现Runnable接口
class TicketRunnable2 implements Runnable{
//3.定义一个成员变量,用来保存票数100
/*由于自定义类对象只创建了一次,所以票数被所有线程对象Thread类的对象共享*/
int tickets = 100;
Object o = new Object();
//2.添加接口中未实现的方法,方法里是我们的业务
@Override
public void run() {
//4.1循环卖票
while(true){
synchronized (o) {
//synchronized (TicketRunnable2.class) {
if (tickets > 0 ){
//8.让线程休眠10ms,增加线程状态切换的概率和出错的概率
try {
Thread.sleep(10);//让当前线程休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.2打印当前正在售票的线程名称 & 票数-1
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
}
//4.3设置死循环的出口,没票了就停止卖票
if (tickets <= 0) break;
}
}
}
}
3.同步锁
- 判断程序有没有可能出现线程安全问题,主要有以下三个条件:
在多线程程序中 + 有共享数据 + 多条语句操作共享数据 - synchronized同步关键字
synchronized (锁对象){
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句);
}
3.1 特点
- synchronized同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象类型任意,但注意:必须唯一!
- synchronized同步关键字可以用来修饰方法,称为同步方法
- 同步的缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的
- 但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了