Java
中的equals
和==
浅析先看一段Java
代码:
package test;
public class HelloWorld {
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1==str2);
System.out.println(str1.equals(str2));
}
}
输出结果:
false
true
输出结果不一样?==和equals方法之间的区别是什么?
eg
:
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int n=3;
int m=3;
System.out.println(n==m);
String str = new String("hello");
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1==str2);
str1 = str;
str2 = str;
System.out.println(str1==str2);
}
}
输出结果为 true false true
n==m
结果为true
,这个很容易理解,变量n
和变量m
存储的值都为3
,肯定是相等的。而为什么str1
和str2
两次比较的结果不同?要理解这个其实只需要理解基本数据类型变量和非基本数据类型变量的区别。
在Java
中游8
种基本数据类型:
浮点型:float(4 byte), double(8 byte)
`
整型:byte(1 byte), short(2 byte), int(4 byte) , long(8 byte)
`
字符型: char(2 byte)
`
布尔型: boolean
(JVM
规范没有明确规定其所占的空间大小,仅规定其只能够取字面值"true"
和"false"
)
对于这8
种基本数据类型的变量,变量直接存储的是“值”,因此在用关系操作符==来进行比较时,比较的就是 “值” 本身。要注意浮点型和整型都是有符号类型的,而char
是无符号类型的(char
类型取值范围为0~2^16-1
).
也就是说比如:
int n=3;
int m=3;
变量n
和变量m
都是直接存储的"3"
这个数值,所以用==
比较的时候结果是true
。
而对于非基本数据类型的变量,在一些书籍中称作为 引用类型的变量。比如上面的str1
就是引用类型的变量,引用类型的变量存储的并不是 “值”本身,而是于其关联的对象在内存中的地址。比如下面这行代码:
String str1;
这句话声明了一个引用类型的变量,此时它并没有和任何对象关联。
而通过new String("hello")
来产生一个对象(也称作为类String
的一个实例),并将这个对象和str1
进行绑定:
str1= new String("hello");
那么str1
指向了一个对象(很多地方也把str1
称作为对象的引用),此时变量str1
中存储的是它指向的对象在内存中的存储地址,并不是“值”本身,也就是说并不是直接存储的字符串"hello"
。这里面的引用和C/C++
的指针很类似。
因此在用==
对str1
和str2
进行第一次比较时,得到的结果是false
。因此它们分别指向的是不同的对象,也就是说它们实际存储的内存地址不同。
而在第二次比较时,都让str1
和str2
指向了str
指向的对象,那么得到的结果毫无疑问是true
。
equals
方法是基类Object
中的方法,因此对于所有的继承于Object
的类都会有该方法。为了更直观地理解equals
方法的作用,直接看Object
类中equals
方法的实现。
源码:
public boolean equals(Object obj) {
return (this == obj);
}
很显然,在Object
类中,equals
方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。
但是有些朋友又会有疑问了,为什么下面一段代码的输出结果是true
?
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2));
要知道究竟,可以看一下String
类的equals
方法的具体实现,同样在该路径下,String.java
为String
类的实现。
下面是String
类中equals
方法的具体实现:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
可以看出,String
类对equals
方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。
其他的一些类诸如Double,Date,Integer
等,都对equals
方法进行了重写用来比较指向的对象所存储的内容是否相等。
总结来说:
1)对于==
,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址
2)对于equals
方法,注意:equals
方法不能作用于基本数据类型的变量
如果没有对equals
方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date
等类对equals
方法进行了重写的话,比较的是所指向的对象的内容。
这个不是什么项目需求推动,只是自己在这方面使用的少,以前也不喜欢写博客,没有什么技术积累,自己参照网上的例子动手实践一下,加深印象。
因为TabLayout
和ViewPager
分别是属于design
和v4
包下的,所以我们先在app
的build.gradle
中添加:
compile 'com.android.support:design:25.1.0'
compile 'com.android.support:support-v4:25.1.0'
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"s
tools:context=".MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none">
</android.support.v4.view.ViewPager>
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="50dp"
app:tabGravity="fill"
app:tabIndicatorHeight="0dp"
app:tabMode="fixed"
app:tabSelectedTextColor="#FF4081"
app:tabTextColor="#000">
</android.support.design.widget.TabLayout>
</LinearLayout>
TabLayout
中app:tabIndicatorHeight="0dp"
是为了不显示tab
底部的横线,app:tabMode="fixed"
是让底部tab
布局不可滑动。
Acitivity
源代码:package com.example.hankcoder.completequitapp;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import com.example.hankcoder.completequitapp.Fragment.FragmentItem;
public class Main3Activity extends AppCompatActivity {
private TabLayout mTlayout;
private ViewPager mViewPager;
private String[] mTitle;
private TabLayout.Tab mOne;
private TabLayout.Tab mTwo;
private TabLayout.Tab mThree;
private TabLayout.Tab mFour;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.linearlayout_tab_vg);
initDatas();
initViews();
initEvents();
}
private void initDatas() {
mTitle = new String[]{"首页", "分类", "设置", "关于"};
}
private void initViews() {
mTlayout = (TabLayout) findViewById(R.id.tabLayout);
mViewPager = (ViewPager) findViewById(R.id.viewPager);
mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
if (position == 1) {
return new FragmentItem();
} else if (position == 2) {
return new FragmentItem();
} else if (position == 3) {
return new FragmentItem();
}
return new FragmentItem();
}
@Override
public int getCount() {
return mTitle.length;
}
@Override
public CharSequence getPageTitle(int position) {
return mTitle[position];
}
});
mTlayout.setupWithViewPager(mViewPager);
mOne = mTlayout.getTabAt(0);
mTwo = mTlayout.getTabAt(1);
mThree = mTlayout.getTabAt(2);
mFour = mTlayout.getTabAt(3);
mOne.setIcon(getResources().getDrawable(R.mipmap.ic_launcher_round));
mTwo.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
mThree.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
mFour.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
}
private void initEvents() {
mTlayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (tab == mTlayout.getTabAt(0)) {
tab.setIcon(getResources().getDrawable(R.mipmap.ic_launcher_round));
mViewPager.setCurrentItem(0);
} else if (tab == mTlayout.getTabAt(1)) {
tab.setIcon(getResources().getDrawable(R.mipmap.ic_launcher_round));
mViewPager.setCurrentItem(1);
} else if (tab == mTlayout.getTabAt(2)) {
tab.setIcon(getResources().getDrawable(R.mipmap.ic_launcher_round));
mViewPager.setCurrentItem(2);
} else if (tab == mTlayout.getTabAt(3)) {
tab.setIcon(getResources().getDrawable(R.mipmap.ic_launcher_round));
mViewPager.setCurrentItem(3);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
if (tab == mTlayout.getTabAt(0)) {
mOne.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
} else if (tab == mTlayout.getTabAt(1)) {
mTwo.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
} else if (tab == mTlayout.getTabAt(2)) {
mThree.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
}else if (tab == mTlayout.getTabAt(3)){
mFour.setIcon(getResources().getDrawable(R.mipmap.ic_launcher));
}
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
}
直接左右滑动没问题,但是点击时,会有两个tab
显示为红色?
一个线程对共享变量值的修改,能够及时的被其它线程看到。
如果一个变量在多个线程中都有工作副本,那么这个变量就是这几个线程的共享变量。
Java
内存模型(Java Memory Model
)描述了Java
程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
主内存中该变量的一份拷贝
)代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
as-if-serial
: 无论如何重排序,程序执行的结果应该与代码顺序执行结果一致(Java
编译器,运行时和处理器都会保证Java
在单线程下遵循as-if-serial
语义)
注:重排序不会给单线程带来内存可见性问题,多线程中程序交错执行时,重排序可能会造成内存可见性问题
线程1
对共享变量的修改想要被线程2
及时看到,必须要经过如下2
个步骤:
1
中更新过的共享变量刷新到主内存中2
中synchronized
可见性
JMM
关于synchronized
的两条规定:注意:加锁与解锁需要是同一把锁
)volatile
volatile
变量的可见性volatile
变量复合操作的原子性###### volatile
如何实现内存可见性
深入来说:通过加入内存屏障和禁止重排序优化来实现的
volatile
变量执行写操作时,会在写操作后加入一条store
屏障指令volatile
变量执行读操作时,会在读操作前加入一条load
屏障指令通俗的说:volatile
变量在每次被线程访问时,都强迫从主内存中重新读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。
volatile
变量的过程volatile
变量副本的值###### 线程读volatile
变量的过程
volatile
变量的最新值到线程的工作内存中volatile
变量的副本###### volatile
不能保证volatile
变量复合操作的原子性
private int number = 0;
number ++; //不是原子操作
synchronized(this) {
number++;
}
上述代码能保证原子性,属于原子操作
private volatile int number = 0;
上述代码添加volatile
字段,也无法保证原子性
###### volatile
不能保证原子性分析
number++ // 上面说了这步操作实际上分三步执行
number = 5
1.线程A读取number的值
2.线程B读取number的值
3.线程B执行加1操作
4.线程B写入最新的number的值
主内存:number = 6
线程B工作内存:number = 6
线程A工作内存:number = 5
5.线程A执行加1操作
6.线程A写入最新的number
值
结果:两个线程分别进行了加1,但是结果总共才增加1
###### 保证number
自增操作的原子性
synchronized
关键字JDK1.5
以后提供的ReentrantLock
(java.until.concurrent.locks
包下)
private Lock lock = new ReentrantLock();
lock.lock();
try {
number++;
} finally {
lock.unlock();
}
AtomicInterger
(vava.util.concurrent.atomic
包下)###### volatile
使用场合
要在多线程中安全使用volatile
变量,必须同时满足:
1.对变量的写入操作不依赖其当前值
number++
, count = count * 5
等synchronized
和voaltile
比较volatile
不需要加锁,比synchronized
更轻量级,不会阻塞线程;volatile
读相当于加锁,volatile
写相当于解锁synchronized
既能保证可见性,又能保证原子性,而volatile
只能保证可见性,无法保证原子性Java
内存模型FAQ
(一
)什么是内存模型在多核系统中,处理器一般有一层或者多层的缓存,这些的缓存通过加速数据访问(因为数据距离处理器更近)和降低共享内存在总线上的通讯(因为本地缓存能够满足许多内存操作)来提高CPU
性能。缓存能够大大提升性能,但是它们也带来了许多挑战。例如,当两个CPU
同时检查相同的内存地址时会发生什么?在什么样的条件下它们会看到相同的值?
在处理器层面上,内存模型定义了一个充要条件,“让当前的处理器可以看到其他处理器写入到内存的数据”以及“其他处理器可以看到当前处理器写入到内存的数据”。有些处理器有很强的内存模型(strong memory model
),能够让所有的处理器在任何时候任何指定的内存地址上都可以看到完全相同的值。而另外一些处理器则有较弱的内存模型(weaker memory model
),在这种处理器中,必须使用内存屏障(一种特殊的指令)来刷新本地处理器缓存并使本地处理器缓存无效,目的是为了让当前处理器能够看到其他处理器的写操作或者让其他处理器能看到当前处理器的写操作。这些内存屏障通常在lock
和unlock
操作的时候完成。内存屏障在高级语言中对程序员是不可见的。
在强内存模型下,有时候编写程序可能会更容易,因为减少了对内存屏障的依赖。但是即使在一些最强的内存模型下,内存屏障仍然是必须的。设置内存屏障往往与我们的直觉并不一致。近来处理器设计的趋势更倾向于弱的内存模型,因为弱内存模型削弱了缓存一致性,所以在多处理器平台和更大容量的内存下可以实现更好的可伸缩性
“一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。
此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。
看下面代码展示的一个简单例子:
ClassReordering {
int x = 0, y = 0;
public void writer() {
x = 1;
y = 2;
}
public void reader() {
int r1 = y;
int r2 = x;
}
}
让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到1
。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是r1
变量的值是2
,但是r2
变量的值为0
。
Java
内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java
内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。
Java
包含了几个语言级别的关键字,包括:volatile
, final
以及synchronized
,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java
内存模型定义了volatile
和synchronized
的行为,更重要的是保证了同步的java
程序在所有的处理器架构下面都能正确的运行。
Java
内存模型FAQ
(十
)volatile
是干什么用的Volatile
字段是用于线程间通讯的特殊字段。每次读volatile
字段都会看到其它线程写入该字段的最新值;实际上,程序员之所以要定义volatile
字段是因为在某些情况下由于缓存和重排序所看到的陈旧的变量值是不可接受的。编译器和运行时禁止在寄存器里面分配它们。它们还必须保证,在它们写好之后,它们被从缓冲区刷新到主存中,因此,它们立即能够对其他线程可见。相同地,在读取一个volatile
字段之前,缓冲区必须失效,因为值是存在于主存中而不是本地处理器缓冲区。在重排序访问volatile
变量的时候还有其他的限制。
在旧的内存模型下,访问volatile
变量不能被重排序,但是,它们可能和访问非volatile
变量一起被重排序。这破坏了volatile
字段从一个线程到另外一个线程作为一个信号条件的手段。
在新的内存模型下,volatile
变量仍然不能彼此重排序。和旧模型不同的时候,volatile
周围的普通字段的也不再能够随便的重排序了。写入一个volatile
字段和释放监视器有相同的内存影响,而且读取volatile
字段和获取监视器也有相同的内存影响。事实上,因为新的内存模型在重排序volatile
字段访问上面和其他字段(volatile
或者非volatile
)访问上面有了更严格的约束。当线程A写入一个volatile
字段f
的时候,如果线程B
读取f
的话 ,那么对线程A
可见的任何东西都变得对线程B
可见了。
如下例子展示了volatile
字段应该如何使用:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
假设一个线程叫做“writer
”,另外一个线程叫做“reader
”。对变量v
的写操作会等到变量x
写入到内存之后,然后读线程就可以看见v
的值。因此,如果reader
线程看到了v
的值为true
,那么,它也保证能够看到在之前发生的写入42
这个操作。而这在旧的内存模型中却未必是这样的。如果v
不是volatile
变量,那么,编译器可以在writer
线程中重排序写入操作,那么reader
线程中的读取x
变量的操作可能会看到0
。
实际上,volatile
的语义已经被加强了,已经快达到同步的级别了。为了可见性的原因,每次读取和写入一个volatile
字段已经像一个半同步操作了
重点注意:对两个线程来说,为了正确的设置happens-before
关系,访问相同的volatile
变量是很重要的。以下的结论是不正确的:当线程A
写volatile
字段f
的时候,线程A
可见的所有东西,在线程B
读取volatile
的字段g
之后,变得对线程B
可见了。释放操作和获取操作必须匹配(也就是在同一个volatile
字段上面完成)。
Java
内存模型FAQ
(七
)同步会干些什么呢同步有几个方面的作用。最广为人知的就是互斥 ——一次只有一个线程能够获得一个监视器,因此,在一个监视器上面同步意味着一旦一个线程进入到监视器保护的同步块中,其他的线程都不能进入到同一个监视器保护的块中间,除非第一个线程退出了同步块。
但是同步的含义比互斥更广。同步保证了一个线程在同步块之前或者在同步块中的一个内存写入操作以可预知的方式对其他有相同监视器的线程可见。当我们退出了同步块,我们就释放了这个监视器,这个监视器有刷新缓冲区到主内存的效果,因此该线程的写入操作能够为其他线程所见。在我们进入一个同步块之前,我们需要获取监视器,监视器有使本地处理器缓存失效的功能,因此变量会从主存重新加载,于是其它线程对共享变量的修改对当前线程来说就变得可见了。
依据缓存来讨论同步,可能听起来这些观点仅仅会影响到多处理器的系统。但是,重排序效果能够在单一处理器上面很容易见到。对编译器来说,在获取之前或者释放之后移动你的代码是不可能的。当我们谈到在缓冲区上面进行的获取和释放操作,我们使用了简述的方式来描述大量可能的影响。
新的内存模型语义在内存操作(读取字段,写入字段,锁,解锁)以及其他线程的操作(start
和 join
)中创建了一个部分排序,在这些操作中,一些操作被称为happen before
其他操作。当一个操作在另外一个操作之前发生,第一个操作保证能够排到前面并且对第二个操作可见。这些排序的规则如下:
线程中的每个操作happens before
该线程中在程序顺序上后续的每个操作。
解锁一个监视器的操作happens before
随后对相同监视器进行锁的操作。
对volatile
字段的写操作happens before
后续对相同volatile
字段的读取操作。
线程上调用start()
方法happens before
这个线程启动后的任何操作。
一个线程中所有的操作都happens before
从这个线程join()
方法成功返回的任何其他线程。(注意思是其他线程等待一个线程的jion()
方法完成,那么,这个线程中的所有操作happens before
其他线程中的所有操作)
这意味着:任何内存操作,这个内存操作在退出一个同步块前对一个线程是可见的,对任何线程在它进入一个被相同的监视器保护的同步块后都是可见的,因为所有内存操作happens before
释放监视器以及释放监视器happens before
获取监视器。
其他如下模式的实现被一些人用来强迫实现一个内存屏障的,不会生效:
synchronized (new Object()) {}
这段代码其实不会执行任何操作,你的编译器会把它完全移除掉,因为编译器知道没有其他的线程会使用相同的监视器进行同步。要看到其他线程的结果,你必须为一个线程建立happens before
关系。
重点注意:对两个线程来说,为了正确建立happens before
关系而在相同监视器上面进行同步是非常重要的。以下观点是错误的:当线程A
在对象X
上面同步的时候,所有东西对线程A
可见,线程B
在对象Y
上面进行同步的时候,所有东西对线程B
也是可见的。释放监视器和获取监视器必须匹配(也就是说要在相同的监视器上面完成这两个操作),否则,代码就会存在“数据竞争”。