作者:爱塔居
专栏:JavaEE
作者简介:大三学生,喜欢总结与分享~
文章目录
目录
文章目录
章节回顾
一、wait 和notify
二、设计模式
2.1 单例模式
章节回顾
线程安全
1.一个线程不安全的案例(两个线程各自自增5w次,结果不是10w)
2.线程不安全的原因
(1)抢占式执行,随机调度。线程中的代码执行到任意一行,都随意可能被切换出去。
(2)多个线程同时修改同一个变量。
(3)修改操作不是原子的。
(4)内存可见性(volatile)编译器可能会对我们的代码进行优化。
一个线程频繁读,一个线程修改
(5)指令重排序。
除了这些原因,还有其他原因。
3.解决方案 加锁
在线程1加锁的过程中,线程2无法把自己的指令插入到线程1的修改过程中。
synchronized指定一个“锁对象”
4.volatile
关于volatile和内存可见性补充
内存可见性:
t1频繁读取主内存,效率比较低,就被优化成直接读自己的工作内存。
t2修改了主内存的结果,由于t1没有读主内存,导致修改不能被识别到。
工作内存=>CPU寄存器
主内存=>内存
工作内存和主内存都是由英文work memory和main memory翻译来的。所以,工作内存不一定非要是内存,可以是记忆,存储区,不一定是特指“内存条”。
这一套说法,也称为JMM(java memory model)
java是跨平台的。
1.兼容多种操作系统
2.兼容多种硬件设备
不同的硬件,其实差别很大。cpu和cpu之间,差别就会比较大。
像以前的cpu,上面只有寄存器。现在的cpu上面还有缓存。
而且有的cpu缓存还有好几个,L1,L2,L3,(现在常见的cpu都是3级缓存)
工作内存准确来说,代表cpu寄存器+缓存(CPU内部存储数据的空间)
cpu读储存器速度比读内存快3-4个数量级。
缓存读取速度介于寄存器和内存之间。
L1最快,空间最小(仍然比寄存器慢)
L3最快,空间最大(仍然比内存快很多)
实际上cpu尝试读一个内存数据:
1.先看寄存器里有没有
2.没有,看L1有没有
3.没有,看L2有没有
4.没有,看L3有没有
5.没有,看内存有没有
具体缓存的大小,对于程序效率的影响,也看实际的应用场景。
一、wait 和notify
线程的调度是无序的,随机的。但是,也是有一定的需求场景的,希望线程有序执行。
join是一种控制顺序的方式,但是功效有限。
wait就是让某个线程先暂停下来,等一等。
wait主要做三件事:
1.解锁
2.阻塞等待
3.当收到通知的时候,就唤醒,同时尝试重新获取锁。
notify就是把该线程唤醒,能够继续执行。
wait和notify是Object的方法
只要你是个类对象(不是内置类型/基本数据类型),都是可以使用wait和notify。
public class test {
static int i;
public static void main(String argv[]) throws InterruptedException{
Object locker=new Object();
Thread t1=new Thread(()->{
while (true){
try {
System.out.println("wait 开始");
synchronized (locker){
locker.wait();
}
System.out.println("wait 结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(1000);
Thread t2=new Thread(()->{
synchronized (locker){
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}
}
使用外套,阻塞等待会让线程进入WAITING状态。wait也提供了一个带参数的版本,参数指定的是最大等待时间。不带参数的wait是死等,带参数的wait就会等最大时间之后,还没有人通知,就自己唤醒自己。
wait会导致阻塞,竞争锁也会导致阻塞,两种不同的进入阻塞的方式。wait的初心就是为了实现阻塞的效果。
join只能是让t2的线程先执行完,再继续执行t1,一定是串行的
wait、notify,可以让t2执行完一部分,再让t1执行一部分,再让t2去执行,再……
唤醒操作,还有一个notifyAll。可以有多个线程,等待同一个对象。
比如在t1,t2,t3中都调用object.wait。此时在main中调用了object.notify 会随机唤醒上述的一个线程。(另外两个仍然会是waiting状态)
如果是调用了object.notifyAll,此时就会把上述三个线程都唤醒。伺候这三个线程就会重新竞争锁,然后依次执行。
总结:
1.wait需要搭配synchronized使用,sleep不需要。
2.wait是Object的方法,sleep是Thread的静态方法。
wait和sleep都是可以提前唤醒的。
他们最大的区别在于初心不同。
wait解决的是线程之间的顺序控制
sleep单纯是让当前线程休眠一会。
二、设计模式
设计模式,就是软件开发中的棋谱。大佬们针对一些常见场景,总结出来的代码的编写套路。设计模式有很多种。
在校招阶段,主要考察两个设计模式。
1.单例模式
2.工厂模式
设计模式需要大家有一定的开发经验的积累,才好理解。
2.1 单例模式
单例指的是单个实例(instance)对象。类的实例,就是对象。Java中的单例模式,借助java语法,保证某个类,只能够创建出一个实例,而不能new多次。
有些场景,本身就是要求某个概念是单例的。
//把这个类设定为单例
class Singleton{
//唯一的实例的实体
private static Singleton instance=new Singleton();
//被static修饰,该属性是类的属性。JVM中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份了。
//获取到实例的方法
public static Singleton getInstance(){
return instance;
}
//禁止外部new实例
private Singleton(){};
}
public class test {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
}
}
java中实现单例模式是有多种写法的。
主要说两种:
1.饿汉模式(急迫)
2.懒汉模式(从容)
A吃完饭立马洗碗(饿汉行为)
B吃完饭,不洗碗,等下一顿要用碗的时候,再洗碗。(懒汉行为)
通常认为,懒汉模式更好,效率更高。(非必要,不洗碗)
举一个计算机的例子:
打开一个硬盘上的文件,读取文件内容,并显示出来。
饿汉:把文件所有内容都读到内存中,并显示出来
懒汉:只把文件读一小部分,把当前的屏幕填充上,如果用户翻页,再读其他文件内容。
当文件特别大的时候,就可以看出懒汉模式的优势了。
饿汉模式:
//把这个类设定为单例,饿汉
class Singleton{
//唯一的实例的实体
private static Singleton instance=new Singleton();
//被static修饰,该属性是类的属性。JVM中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份了。
//获取到实例的方法
public static Singleton getInstance(){
return instance;
}
//禁止外部new实例
private Singleton(){};
}
public class test {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
}
}
懒汉模式:
//把这个类设定为单例模式中的懒汉模式。
class SingletonLazy{
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){};
}
public class test {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
饿汉模式一开始就把实例创建好了,而懒汉模式是非必要不创建实例。
上述两个代码,是否是线程安全的?多个线程下调用getInstance,是否会出现问题?
饿汉模式,认为是线程安全的,只是读数据。
而在多线程下,懒汉模式可能无法保证创建对象的唯一性。
比如以下情况:
如何解决上诉线程安全问题?
进行加锁,保证判定和new操作是原子性的。
//把这个类设定为单例模式中的懒汉模式。
class SingletonLazy{
private static SingletonLazy instance=null;
synchronized public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){};
}
public class test {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}