这篇主要说说线程安全:
下面是内容:
所谓线程安全就是:控制多个线程对某个资源的访问和修改。
多线程中线程和线程之间不能传递数据:
现在了解下java内存模型(jMM):规定JVM有主内存 ------多线程共享(每个线程有自己的工作内存)
new一个对象的时候也就是被分配到主内
线程操作某对象的顺序:
- 从主内存复制变量副本到工作内存
- 执行代码,修改共享变量值
- 工作内存数据刷新到主内存
多线程有两个概念需要理解:
- 可见性
- 有序性
可见性:当一个共享变量在多线程的工作 内存有副本时,如果一个线程修改了这个共享变量,其他线程应该能看到这个被修改的值,这就是多线程的可见性
有序性:表现在两方面【java用synchronized关键字来保证】
读取:(引用变量)
线程引用变量(其实就是读取)时,不能直接从主内存中引用,如果工作内存没有该变量则:从主内存中拷贝一个副本到工作内存,完成后,线程引用该副本
当同一线程再度引用该字段时,
可能1.直接从工作内存中取副本
可能2,.重新从主内存获得副本
写(给变量赋值);
线程不能直接给主存中字段赋值,它将值指定给工作内存中的变量副本,完成后,这个变量副本同步到主内存中。
synchronized关键字
当一段代码会修改共享变量时,这段代码成为互斥区和临界区
用法:
synchronized(锁){
临界区代码
}
比如为保证银行账户的安全:
public synchronized void add(int number){
balance= balance + num;
}
public synchronized void withdraw(int number){
balance = balance - num;
}
刚才不是说了synchronized的用法是这样的吗:
- synchronized(锁){
- 临界区代码
- }
- 一个线程执行临界代码的步骤
- a.获得同步锁
- b.清空工作内存
- c.从主内存拷贝副本到工作内存
- d.执行代码
- e.将变量从工作内存写回到主内存
- f.释放锁
那么对于public synchronized void add(int num)这种情况,意味着什么呢?其实这种情况,锁就是这个方法所在的对象。
同理,如果方法是public static synchronized void add(int num),那么锁就是这个方法所在的class。
理论上,每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意义的。假如有这样的代码:
1. public class
2. public void
3. new
4. synchronized
5. //do something
6. }
7. }
8. }
上面的代码中:
lock变量作为一个锁存在根本没有意义,因为它根本不是共享对象,每个线程进来都会执行Object lock=new Object();每个线程都有自己的lock,根本不存在锁竞争。
----------------------------------------------------------------------------
有关锁的状态:
每个锁对象有两个队列:就绪队列+阻塞队列:
就绪队列:存储将要获得锁的线程
阻塞队列:存储被阻塞的线程,当一个线程被唤醒(notify)后才会进入就绪队列,等待执行(cpu调用)
当线程a第一次执行account.add()方法时,jvm会检查锁对象account的就绪队列是否有线程在排队,如果有则表明account锁已经被占用了,
由于当线程a是第一次执行,就绪队列为空,所以线程a获得锁,执行account.add()方法,
如果恰好在这个时候线程b要执行account.withdraw方法,jvm检查到线程a正在占用锁,并且没有释放,所以线程b进入就绪队列,等得到锁才能执行
消费者/生产者 模式
一种典型的线程同步模型:
很多时候
1.并不是只要保证线程的互斥就够了,
2.线程之间还需要进行沟通协作
所以才有了生产者消费者模式:
如何让线程主动放弃锁:用wait()方法,wait()是从object来的,所以任何对象有这个方法:
Object lock = new Object();
synchronized(lock){
balance = balance - num;
lock.wait();
}
如果一个线程获得了锁lock,进入同步块,执行lock.wait(),那么这线程会进入锁对象的阻塞队列,如果调用lock.notify()则会通知阻塞队列的线程进入就绪队列
申明一个盘子只能放一个鸡蛋
public class Plate{
List<Object> eggs = new ArrayList<Object>();
public synchronized Object getEgg(){
if(eggs.size()==0){
try {
wait();//自己进入锁对象的阻塞队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object egg = eggs.get(0);
eggs.clear();//清空盘子
notify();//唤醒阻塞队列的某线程到就绪队列
return egg;
}
public synchronized void putEgg(Object egg){
if(eggs.size()!=0){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
eggs.add(egg);//往盘子里放鸡蛋
notify();//唤醒阻塞队列的某线程到就绪队列
}}
声明一个Plate对象为plate,被线程A和线程B共享,A专门放鸡蛋,B专门拿鸡蛋。假设
1 开始,A调用plate.putEgg方法,此时eggs.size()为0,因此顺利将鸡蛋放到盘子,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有线程。
2 又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,自己进入了锁对象的阻塞队列。
3 此时,来了一个B线程对象,调用plate.getEgg方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,因此马上得到锁,开始往盘子里放鸡蛋,此时盘子是空的,因此放鸡蛋成功。
4 假设接着来了线程A,就重复2;假设来料线程B,就重复3。
======================================================
有关map
Hashmap(不安全)允许null key 和null value
HashTable(线程安全)不允许null key
方法 :
clear():
remove(Object key);
put(Object key,Object value);
get(Object key);
containskey(Object key);
containsvalue(Object value);
isEmpty();
两种基本的数据存储机构:数组,链表,哈希表(前面两者的改进)
===================================================================
java内存管理机制:
内存空间逻辑划分:
JVM会把申请的内存从逻辑上划分为三个区域:方法区,堆 ,栈
- 方法区:默认64M,JVM会将加载的java类,类的结构(属性和方法),类静态成员
- 堆:默认64M, 保存对象的属性值。
- 栈:默认1M,程序运行时每当有方法调用时,JVM会在栈中划分一块内存,供局部变量使用
- 方法调用结束,JVM回收该内存
java数据类型:
1、基本数据类型:没封装指针的变量。
声明此类型变量,只会在栈中分配一块内存空间。
2、引用类型:就是底层封装指针的数据类型。
他们在内存中分配两块空间,
第一块内存分配在栈中,只存放内存地址,不存放具体数值,我们也把它叫指针类型的变量,
第二块内存分配在堆中,存放的是具体数值,如对象属性值等。
3、下面我们从一个例子来看一看:
public class Student {
String stuId;
String stuName;
int stuAge;
}
public class TestStudent {
public static void main(String[] args) {
Student zhouxingxing = new Student();
String name = new String("旺旺");
int a = 10;
char b = 'm';
zhouxingxing.stuId = "9527";
zhouxingxing.stuName = "周星星";
zhouxingxing.stuAge = 25;
}
}
(1)类当然是存放在方法区里面的。
Student zhouxingxing = new Student();
这行代码就创建了两块内存空间,
第一个在栈中,名字叫zhouxingxing,它就相当于指针类型的变量,我们看到它并不存放学生的姓名、年龄等具体的数值,而是存放堆中第二块内存的地址,
第二块才存放具体的数值,如学生的编号、姓名、年龄等信息。
(3) int a = 10;
这是 基本数据类型
下图就是本例的内存布置图:
java值传递和引用传递: