当多个线程访问一个对象时,有可能会发生污读,即读取到未及时更新的数据,这个时候就需要线程同步。
线程同步:
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作
其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可能存在以下问题:
举个例子,一个售票口有10张票,当100个人同时去买时,每个人都获取到了有100张票的数据,所以每个人买了一张,导致最后剩下-90张票,线程不同步就会导致这种结果。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
我们写一个例子,使用线程不安全的List来看看效果
public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
可以看到,循环1000次,只存进去998个,重复执行,这个大小还会变化,所以是线程不安全的。
可以使用synchronized把list加锁,就能保证每次都能插入进去。
public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
这样就能够保证线程安全。
也可以使用JUC(java.util.concurrent
)包下的线程安全的列表CopyOnWriteArrayList,代码如下
import java.util.concurrent.CopyOnWriteArrayList;
public class MyThread{
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}
使用CopyOnWriteArrayList就可以不需要synchronized关键字实现线程安全
查看源代码可以发现,CopyOnWriteArrayList实现了List<E>接口
然后再add方法中使用了synchronized来加锁,和我们上面的操作方法一致
//CopyOnWriteArrayList中的add()方法
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
死锁的条件
只要破坏后三个条件之一就可以避免死锁,可以使用银行家算法等方法。
先写一个不使用锁的例子
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread);
thread1.start();
thread2.start();
thread3.start();
}
public static int tickets = 10;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
}
}
}
执行后发现顺序完全是乱的
使用ReentrantLock(可重入锁)来把相关代码加锁,即可实现按顺序调用
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread);
thread1.start();
thread2.start();
thread3.start();
}
public static int tickets = 10;
final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
这样也可以实现线程同步。
Java提供的线程通信方法
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
均是0bject类的方法都,只能在同步方法或者同步代码块中使用,否则会抛出llegalMonitorStateException
首先定义一个生产者类
//生产者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产第" + i + "个");
container.push(new Product(i));
}
}
}
生产者不断往缓冲区添加产品,然后定义一个消费者类
//消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费第" + container.pop().id + "个");
try {
Thread.sleep(500);
} catch (InterruptedException ignored) { }
}
}
}
消费者不断在缓冲区去除产品,这里添加一个sleep来模拟真实效果
最后定义缓冲区
//缓冲区
class SynContainer {
//容器大小
Product[] products = new Product[10];
//计数器
int count = 0;
//生产者放入产品
public synchronized void push(Product product) {
//如果满了,通知消费者,生产者等待,否则放入产品
if (count == products.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[count++] = product;
this.notifyAll();
}
//消费者消费产品
public synchronized Product pop() {
if (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
return products[--count];
}
}
缓冲区的两个方法都是使用synchronized修饰,保证能够执行完整,然后根据容器大小来判断是否让生产者以及消费者线程等待
当容器中没有产品时,通知消费者等待,生产者线程开始,当产品满时,通知生产者等待,消费者线程开始。
最后补上产品类
//产品
class Product {
//产品编号
int id;
public Product(int id) {
this.id = id;
}
}
类定义和上面类似,只不过在产品类中添加了一个信号量来区分是否有产品,不需要一个缓冲区
//生产者
class Producer extends Thread {
Product product;
public Producer(Product product) {
this.product = product;
}
//生产
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.push("产品" + i);
}
}
}
//消费者
class Consumer extends Thread {
Product product;
public Consumer(Product product) {
this.product = product;
}
//消费
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.pop();
}
}
}
//产品
class Product {
String product;
boolean flag = true;
//生产
public synchronized void push(String product) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("生产了" + product);
//通知消费
this.notifyAll();
this.product = product;
this.flag = !this.flag;
}
//消费
public synchronized void pop() {
if (flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("消费了" + this.product);
//通知生产者
this.notifyAll();
this.flag = !this.flag;
}
}
这样也可以解决生产者和消费者问题
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
JDK 5.0起提供了线程池相关API: ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
代码演示
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
这样就可以实现通过线程池来管理线程
Java多线程(上)https://www.cnblogs.com/chaofanq/p/15024558.html
原文:https://www.cnblogs.com/chaofanq/p/15055853.html