一、概述:
多线程同时共享同一个全局变量或静态变量做写的操作时,会发生数据冲突问题,也就是线程安全问题。
二、同步机制:
同步机制是Java为了解决线程安全问题引入的机制,该机制利用锁的概念,使多线程共享的数据只让一个线程进行读写操作,当前线程执行完成后释放锁,然后才让其他线程进行读写操作。Java中的锁主要可分为内置锁和显示锁。
1. 内置锁:
每个Java对象都可以看作实现同步的锁,称为内置锁,又称互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。内置锁使用synchronized关键字实现,synchronized关键字有两种用法:
(1)同步代码块:
格式:
synchronized(锁对象){
可能出现线程安全的代码(访问共享数据)
}
//同步代码块中的锁对象,可以是任意的Java对象
//为了保证多线程同步,使用的锁对象必须是同一个
示例:
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){
//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
}
}
}
}
(2)同步方法:
步骤:
①把访问共享数据的代码抽取出来,放到一个方法中;
②在方法中添加synchronized字符修饰。
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能出现线程安全的代码(访问共享数据)
}
//同步代码块中的锁对象,可以是任意的Java对象
//为了保证多线程同步,使用的锁对象必须是同一个
示例:
public class Ticket implements Runnable{
private int ticket = 100;
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
public synchronized void sellTicket(){
if(ticket>0){
//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
}
}
}
同步方法的锁对象是隐含对象,对于非static方法来说,其锁对象就是该实现类对象(this),而static方法由于其在实现类对象创建之前就已经加载进内存,所以无法获取实现类对象,所以其锁对象是本类的class属性(class文件对象)。
2. 显示锁:
java.util.concurrent.locks.Lock 接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
(1)Lock接口中的方法:
①void lock();
②void unlock();
java.util.concurrent.locks.ReentrantLock implements Lock接口,所以我们使用Lock锁时通过创建ReentrantLock对象来获取锁。
(2)步骤:
①在成员位置创建一个ReentrantLock对象;
②在可能出现问题的代码前调用Lock接口中的lock()方法获取锁;
③在可能出现问题的代码后调用Lock接口中的unlock()方法释放锁;
示例:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){
//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
} lock.unlock();
}
}
}
三、ThreadLocal:
ThreadLocal为线程提供一个局部变量。使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
1. ThreadLocal的接口方法:
• void set(Object value);设置当前线程的线程局部变量的值。
• public Object get();该方法返回当前线程所对应的线程局部变量。
• public void remove();将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• protected Object initialValue();返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
2. 实现原理:
ThreadLocal 是一个泛型类,保证可以接受任何类型的对象,因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
Map.put(“当前线程”,值对象)。
示例:
class Res {
// 生成序列号共享变量
public static Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
};
};
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class ThreadLocaDemo2 extends Thread {
private Res res;
public ThreadLocaDemo2(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
}
}
四、多线程的三大特性:
1. 原子性:
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就是为了保证数据一致,线程安全。
2. 可见性:
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
本地私有内存存放的是共享变量的副本,线程操作共享变量,首先操作的是自己本地内存的副本,当同一时刻只有一个线程操作共享变量时,该线程操作完毕本地内存,然后会刷新到主内存,然后主内存会通知另一个线程,进而更新;但是如果同一时刻有多个线程操作共享变量,会来不及更新主内存进而通知其他线程更新变量,就会出现冲突问题。
3. 有序性:
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。