一、多行代码的线程安全问题【重点】
1、执行顺序的问题
火车卖票问题
public class AAA implements Runnable {
//定义火车站一共有100张票
int ticket = 100;
@Override
public void run() {
//循环
while(true){
//判断有没有票
if(ticket <= 0){
break;
}
//如果有票就卖票
System.out.println(Thread.currentThread().getName() + "卖出了" + ticket + "号票");
//给票减一
ticket--;
}
}
}
public class Test01 {
public static void main(String[] args) {
AAA a = new AAA();
//开启线程
Thread t1 = new Thread(a);
t1.setName("窗口一");
t1.start();
//开启线程
Thread t2 = new Thread(a);
t2.setName("窗口二");
t2.start();
}
}
图解
2、synchronized关键字
表示同步,同步的意思是一个线程在执行的时候别的线程只能等待。一个线程执行结束之后别的线程才能执行。
3、同步代码块
格式
synchronized(锁对象){
同步代码
}
同步锁
锁对象可以是任意类型的对象
多个线程如果要同步必须使用同一个对象作为锁
代码演示
public class AAA implements Runnable {
//定义火车站一共有100张票
int ticket = 100;
@Override
public void run() {
//循环
while(true){
//同步代码块
synchronized ("abc") {
//判断有没有票
if (ticket <= 0) {
break;
}
//先获取当前线程对象,再获取线程名字
String name = Thread.currentThread().getName();
//如果有票就卖票
System.out.println(name + "卖出了" + ticket + "号票");
//给票减一
ticket--;
}
}
}
}
public class Test01 {
public static void main(String[] args) {
AAA a = new AAA();
//开启线程
Thread t1 = new Thread(a);
t1.setName("窗口一");
t1.start();
//开启线程
Thread t2 = new Thread(a);
t2.setName("窗口二");
t2.start();
}
}
4、同步方法
格式
public synchronized void method(){
同步代码
}
同步方法的锁对象
同步方法里面也是有锁对象的,但是锁对象不需要我们指定,同步方法的锁对象是固定的。
非静态同步方法:this (代表当前类的对象)
静态同步方法: 类的字节码对象(每一个类只有一个.class对象,所以这个对象一定是唯一的)
代码演示
public class AAA implements Runnable {
//定义火车站一共有100张票
int ticket = 100;
@Override
public void run() {
//循环
while(true){
//判断有没有票
if(ticket <= 0){
break;
}
//调用方法
method();
}
}
//定义同步方法
public synchronized void method(){
//再次判断
if(ticket > 0) {
//先获取当前线程对象,再获取线程名字
String name = Thread.currentThread().getName();
//如果有票就卖票
System.out.println(name + "卖出了" + ticket + "号票");
//给票减一
ticket--;
}
}
}
public class Test01 {
public static void main(String[] args) {
AAA a = new AAA();
//开启线程
Thread t1 = new Thread(a);
t1.setName("窗口一");
t1.start();
//开启线程
Thread t2 = new Thread(a);
t2.setName("窗口二");
t2.start();
}
}
5、Lock锁
Lock锁的方式更符合面向对象的调用方式。更符合程序员的写代码习惯。
Lock是一个接口,有一个子类ReentrantLock
两个方法
public void lock() :加同步锁
public void unlock() :释放同步锁
代码演示
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AAA implements Runnable {
//定义火车站一共有100张票
int ticket = 100;
//创建锁对象
Lock lock = new ReentrantLock();
public void run() {
//循环
while(true){
//加锁
lock.lock();
//判断有没有票
if(ticket <= 0){
lock.unlock();
break;
}
//先获取当前线程对象,再获取线程名字
String name = Thread.currentThread().getName();
//如果有票就卖票
System.out.println(name + "卖出了" + ticket + "号票");
//给票减一
ticket--;
//解锁
lock.unlock();
}
}
}
public class Test01 {
public static void main(String[] args) {
AAA a = new AAA();
//开启线程
Thread t1 = new Thread(a);
t1.setName("窗口一");
t1.start();
//开启线程
Thread t2 = new Thread(a);
t2.setName("窗口二");
t2.start();
}
}
二、并发包
之前学习的集合类型都是会有线程安全问题的,如果遇到了多线程情况的,需要用并发包解决问题。
1、CopyOnWriteArrayList
ArrayList和CopyOnWriteArrayList效果演示
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
public class AAA extends Thread {
//定义集合
//static ArrayList<Integer> list = new ArrayList<>();
//使用并发包集合
static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
@Override
public void run() {
//给集合添加10000个元素
for (int i = 0; i < 10000; i++) {
list.add(i);
}
}
}
public class Test01 {
public static void main(String[] args) throws InterruptedException {
//开启线程
new AAA().start();
new AAA().start();
//让主线程睡眠让循环先执行
Thread.sleep(2000);
//打印集合长度
System.out.println(AAA.list.size());
//问题一:可能长度小于20000
//问题二:可能出现索引越界异常
}
}
2、CopyOnWriteArraySet
HashSet和CopyOnWriteArraySet效果演示
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;
public class BBB extends Thread {
//定义集合
//static HashSet<Integer> set = new HashSet<>();
//定义并发包集合
static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
@Override
public void run() {
//给集合添加10000个元素
for (int i = 0; i < 10000; i++) {
set.add(i);
}
}
}
public class Test02 {
public static void main(String[] args) throws InterruptedException {
//开启线程
new BBB().start();
new BBB().start();
//让主线程睡眠让循环先执行
Thread.sleep(2000);
//打印集合长度
System.out.println(BBB.set.size());
//问题:集合的长度大于10000
}
}
3、ConcurrentHashMap
3.1、HashMap和Hashtable和ConcurrentHashMap效果演示
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class CCC extends Thread {
//创建集合
//static HashMap<Integer,Integer> map = new HashMap<>();
//使用Hashtable集合
//static Hashtable<Integer,Integer> map = new Hashtable<>();
//使用ConcurrentHashMap集合
static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
@Override
public void run() {
//给集合添加10000个元素
for (int i = 0; i < 10000; i++) {
map.put(i,i);
}
}
}
public class Test03 {
public static void main(String[] args) throws InterruptedException {
//开启线程
new CCC().start();
new CCC().start();
//让主线程睡眠
Thread.sleep(2000);
//打印集合长度
System.out.println(CCC.map.size());
//问题:集合的长度大于10000
}
}
2、Hashtable和ConcurrentHashMap的速度区别【重点】
Hashtable执行速度慢
ConcurrentHashMap执行速度快
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class CCC extends Thread {
//创建集合
//static HashMap<Integer,Integer> map = new HashMap<>();
//使用Hashtable集合
// static Hashtable<Integer,Integer> map = new Hashtable<>();
//使用ConcurrentHashMap集合
static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
@Override
public void run() {
//获取系统当前时间
long time1 = System.currentTimeMillis();
//给集合添加10000个元素
for (int i = 0; i < 10000; i++) {
map.put(i,i);
}
//获取系统当前时间
long time2 = System.currentTimeMillis();
System.out.println( (time2-time1) + "毫秒");
}
}
public class Test04 {
public static void main(String[] args) {
//创建1000个线程
for (int i = 0; i < 1000; i++) {
new CCC().start();
}
}
}
3、速度区别的原因
Hashtable方法都是同步的,一个线程在执行的时候,别的线程只能等待。
public synchronized V put(K key, V value) {}
ConcurrentHashMap用到了CAS机制和部分同步代码块。
4、悲观锁和乐观锁
CAS机制称为乐观锁,执行效率高
同步机制称为悲观锁,执行效率低