synchronized简介:其中文意思为同步,所以也称之为同步锁。其作用是保证在同一时刻被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。synchronized
是Java中解决并发问题的一种最常用方法,也是最简单的一种方法
对于前文中的那个线程不安全代码,我们可以使用synchronized
修饰decrease
方法
class Counter{
public int tickets = 100000;
public synchronized void decrease(){
for(int i = 0; i < 50000; i++) {
tickets--;
}
}
}
public class TestDemo {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
//下面两个线程,每个线程都会counter进行5W次自减
//正确结果理应为0
Thread thread1 = new Thread(){
@Override
public void run(){
counter.decrease();
}
};
Thread thread2 = new Thread(){
@Override
public void run(){
counter.decrease();
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("counter: " + counter.tickets);
}
}
可以看到,此时counter.tickets
正确自减为0(多次运行结果仍为0)
互斥:synchronized
起到互斥效果,某个线程执行到某个对象的synchronized
中时,其他线程如果也执行到同一个对象的synchronized
时就会阻塞等待
synchronized
修饰的代码块synchronized
修饰的代码块可重入:synchronized
同步块对同一条线程来说时可重入的,不会存在自己把自己锁死的情况
所谓自己把自己锁死,是指一个线程没有释放锁,然后又尝试再次加锁。对于不可重入锁来说,第二次加锁的时候需要等待第一次加锁释放,但释放操作也必须由该线程来完成,所以这就造成了矛盾,形成了死锁
public class TestDemo2 {
static class Counter{
//第一次加锁,成功
public synchronized void decrease(){
//第二次加锁,锁已经被占用,阻塞等待
synchronized (this){
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread = new Thread(){
@Override
public void run(){
counter.decrease();
}
};
thread.start():
}
}
但好在我们的synchronized
是可重入锁,不会有上述问题发生。这因为在synchronized
内部包含了线程持有者和计数器这两个信息
①:加锁时一定要清楚你需要加锁的代码是哪一部分,否则代码逻辑可能会出现问题,凡是不被synchronized
修饰的代码都是并发执行
for
循环直接写在被synchronized
修饰的decrease
方法中,那么这就不是多线程了,而是单线程class Counter{
public int tickets = 100000;
public synchronized void decrease(Thread thread){
for(int i = 0; i < 50000; i++) {
System.out.println(thread.getName() + "执行中" + "第" + i + "次自减");
tickets--;
}
}
}
public class TestDemo {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
//下面两个线程,每个线程都会counter进行5W次自减
//正确结果理应为0
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
counter.decrease(this);
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
counter.decrease(this);
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("counter: " + counter.tickets);
}
}
②:锁住的代码越多,那么锁的粒度就越大,反之,锁的粒度越小
③:不是说加了锁就一定能保证线程安全,只有正确的加锁方式才能保证线程安全
④:Java多线程的封装性要比C++强一点,我们可以做以对比。如下是C++多线程代码,和上述代码逻辑基本一致,不同的是生成了4个线程,轮流进入函数减少tickets
,其中pthread_mutex_lock
和pthread_mutex_uolock
分别就是加锁和释放锁
#include
#include
#include
int tickets=1000;
pthread_mutex_t lock;//申请一把锁
void scarmble_tickets(void* arg)
{
long int ID=(long int)arg;//线程ID
while(1)//多个线程循环抢票
{
pthread_mutex_lock(&lock);//那个线程先到,谁就先锁定资源
if(tickets>0)
{
usleep(1000);
printf("线程%ld号抢到了一张票,现在还有%d张票\n",ID,tickets);
tickets--;
pthread_mutex_unlock(&lock);//抢到票就解放资源
}
else
{
pthread_mutex_unlock(&lock);//如果没有抢到也要释放资源,否则线程直接退出,其他线程无法加锁
break;
}
}
}
int main()
{
int i=0;
pthread_t tid[4];//4个线程ID
pthread_mutex_init(&lock,NULL);//初始化锁
for(i=0;i<4;i++)
{
pthread_create(tid+1,NULL,scarmble_tickets,(void*)i);//创建4个线程
}
for(i=0;i<4;i++)
{
pthread_join(tid+1,NULL);//线程等待
}
pthread_mutex_destroy(&lock);//销毁锁资源
return 0;
}
⑤:可以使用synchronized
的地方有,如下
this
):锁的是counter
对象counter
类的对象1:修饰普通方法
public synchronized void decrease(){
for(int i = 0; i < 50000; i++) {
tickets--;
}
}
2:修饰静态方法
public synchronized static void decrease(){
for(int i = 0; i < 50000; i++) {
tickets--;
}
}
3:修饰代码块
public synchronized void decrease(){
synchronized (this){
tickets--;
}
}
synchronized深刻理解之锁对象:可以看出,C++在实现并发时全局有一把锁
然后每个线程在进入需要并发执行的代码时会持有锁,进行锁竞争。而在Java中,任意对象都可以作为锁对象,所以无需关心这个锁对象究竟是谁,只关心它们是否锁同一对象,只要锁的是同一对象,那么就有锁竞争。在实际情况中,会有很多种写法,这些写法有的可以形成锁竞争,有的则不可以,现在讨论如下,依次帮助大家理解这部分概念
写法1:针对同一对象counter
加锁,可以形成锁竞争
this
指的就是counter
对象class Counter{
public int tickets = 100000;
public void decrease(){
synchronized (this){
tickets--;
}
}
}
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
写法2:针对不同对象counter1
和counter2
加锁,无法形成锁竞争
this
分别指的是counter1
和counter2
对象class Counter{
public int tickets = 100000;
public void decrease(){
synchronized (this){
tickets--;
}
}
}
private static Counter counter = new Counter();
private static Counter counter2 = new Counter();
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter2.decrease();
}
}
};
写法3:使用一个专门的locker
对象作为锁对象,this
改为locker
,然后针对同一counter
加锁。此时由于counter
是同一个,所以locker
也是一样的,因此可以形成锁竞争
class Counter{
public int tickets = 100000;
public Object locker = new Object();
public void decrease(){
synchronized (locker){
tickets--;
}
}
}
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
写法4:结合写法2和写法3,自然无法形成锁竞争,因为是不同的对象
写法5:将locker
设为static
,此时locker为静态成员,由于静态成员只有一份,所以即便有两个不同的对象counter
和counter2
,也是能够形成锁竞争的
class Counter{
public int tickets = 100000;
static private Object locker = new Object();
public void decrease(){
synchronized (locker){
tickets--;
}
}
}
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter2.decrease();
}
}
};
写法6:将Counter.class即类对象作为锁对象,由于类对象在JVM中只有一个,所以可以形成锁竞争
class Counter{
public int tickets = 100000;
public void decrease(){
synchronized (Counter.class){
tickets--;
}
}
}
Thread thread1 = new Thread("thread1"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter.decrease();
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run(){
for(int i = 0; i < 50000; i++) {
counter2.decrease();
}
}
};
①:直接修饰普通方法
this
public synchronized void decrease(){
for(int i = 0; i < 50000; i++) {
tickets--;
}
}
②:修饰代码块
synchronized
里面的锁对象为this
,所以谁调用decrease
,就针对谁加锁。这里针对是counter
对象,所以两个线程执行到这里时就会出现互斥public synchronized void decrease(){
synchronized (this){
tickets--;
}
}
Java标准库中的线程安全类:多线程环境下,Java标准库中很多都是线程不安全的,所以在使用时要慎重
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
以下是线程安全的,使用锁机制
Vector
HashTable
ConcurrentHashMap
StringBuffer
String
(虽然没有加锁,但是它不涉及修改,所以被视为是线程安全的)volatile关键字:被volatile
修饰的变量,能够保证“内存可见性”。其作用主要是用来避免数据不一致的情形发生。这是因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,也就是读取数据时更加倾向于读取寄存器中的数据,这就有可能读取到脏数据
如下代码中,有一个类Counter
,内有一变量counter
,初始值为0。线程thread1
在counter
始终为0的情况下会一直死循环,直到counter
不为0;线程thread2
则随时接受输入,如果输入一个不为0的整数,那么thread1就会结束循环并停止
import java.util.Scanner;
public class TestDmmo3 {
static class Counter{
public int count = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(){
@Override
public void run(){
while(counter.count == 0){
}
System.out.println("线程1执行结束");
}
};
thread1.start();
Thread thread2 = new Thread(){
@Override
public void run(){
System.out.println("(线程2)输入一个整数:");
Scanner scanner = new Scanner(System.in);
counter.count = scanner.nextInt();
}
};
thread2.start();
}
}
但程序运行后,无论输入的整数是多少,线thread1
始终无法结束循环。其原因就是上文中所提到,编译器做了优化,它认为counter
是一个始终不变化的量,就会直接将其移入缓存或寄存器以加快读取速度
解决方法就是使用volatile
关键字修饰,让其强制从内存读取
static class Counter{
public volatile int count = 0;
}
volatile不保证原子性:volatile和synchronized有着本质区别。synchronized能够保证原子性,而volatile不能保证,只能保证内存可见性
volatile
比较合适volatile
无能为力如下代码,可以看到即便使用volatile
仍然无法保证原子性